add monster panel to admin
This commit is contained in:
parent
0c756c425f
commit
59724dee81
BIN
data/dk.db
BIN
data/dk.db
Binary file not shown.
@ -105,6 +105,15 @@ func ByImmunity(immunityType int) ([]*Monster, error) {
|
||||
return monsters, err
|
||||
}
|
||||
|
||||
func ByName(name string) (*Monster, error) {
|
||||
var monster Monster
|
||||
err := database.Get(&monster, "SELECT * FROM monsters WHERE name = %s COLLATE NOCASE", name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("monster with name '%s' not found", name)
|
||||
}
|
||||
return &monster, nil
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (m *Monster) IsHurtImmune() bool {
|
||||
return m.Immune == ImmuneHurt
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"dk/internal/components"
|
||||
"dk/internal/database"
|
||||
"dk/internal/helpers"
|
||||
"dk/internal/models/monsters"
|
||||
"dk/internal/models/news"
|
||||
"dk/internal/models/spells"
|
||||
"dk/internal/models/towns"
|
||||
@ -46,6 +47,11 @@ func RegisterAdminRoutes(app *sushi.App) {
|
||||
group.Post("/spells/new", adminSpellCreate)
|
||||
group.Get("/spells/:id", adminSpellEdit)
|
||||
group.Post("/spells/:id", adminSpellUpdate)
|
||||
group.Get("/monsters", adminMonstersIndex)
|
||||
group.Get("/monsters/new", adminMonsterNew)
|
||||
group.Post("/monsters/new", adminMonsterCreate)
|
||||
group.Get("/monsters/:id", adminMonsterEdit)
|
||||
group.Post("/monsters/:id", adminMonsterUpdate)
|
||||
}
|
||||
|
||||
func adminIndex(ctx sushi.Ctx) {
|
||||
@ -610,6 +616,204 @@ func checkSpellNameConflict(name string, excludeID int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func adminMonstersIndex(ctx sushi.Ctx) {
|
||||
pagination := helpers.Pagination{
|
||||
Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1),
|
||||
PerPage: 30,
|
||||
}
|
||||
|
||||
type MonsterData struct {
|
||||
ID int
|
||||
Name string
|
||||
Level int
|
||||
MaxHP int
|
||||
MaxDmg int
|
||||
Armor int
|
||||
MaxExp int
|
||||
MaxGold int
|
||||
Immune int
|
||||
ImmunityName string
|
||||
}
|
||||
|
||||
var monsterList []*MonsterData
|
||||
err := database.Select(&monsterList, `
|
||||
SELECT id, name, level, max_hp, max_dmg, armor, max_exp, max_gold, immune,
|
||||
CASE immune
|
||||
WHEN 0 THEN 'None'
|
||||
WHEN 1 THEN 'Hurt Spells'
|
||||
WHEN 2 THEN 'Sleep Spells'
|
||||
ELSE 'Unknown'
|
||||
END as immunity_name
|
||||
FROM monsters
|
||||
ORDER BY level ASC, id ASC
|
||||
LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset())
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting monster list for admin index: %s", err.Error())
|
||||
monsterList = make([]*MonsterData, 0)
|
||||
}
|
||||
|
||||
type CountResult struct{ Count int }
|
||||
var result CountResult
|
||||
database.Get(&result, "SELECT COUNT(*) as count FROM monsters")
|
||||
pagination.Total = result.Count
|
||||
|
||||
components.RenderAdminPage(ctx, "Monster Management", "admin/monsters/index.html", map[string]any{
|
||||
"monsters": monsterList,
|
||||
"currentPage": pagination.Page,
|
||||
"totalPages": pagination.TotalPages(),
|
||||
"hasNext": pagination.HasNext(),
|
||||
"hasPrev": pagination.HasPrev(),
|
||||
})
|
||||
}
|
||||
|
||||
func adminMonsterNew(ctx sushi.Ctx) {
|
||||
monster := monsters.New()
|
||||
components.RenderAdminPage(ctx, "Add New Monster", "admin/monsters/edit.html", map[string]any{
|
||||
"monster": monster,
|
||||
})
|
||||
}
|
||||
|
||||
func adminMonsterCreate(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
monster := monsters.New()
|
||||
|
||||
if err := populateMonsterFromForm(ctx, monster); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect("/admin/monsters/new")
|
||||
return
|
||||
}
|
||||
|
||||
if err := monster.Validate(); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect("/admin/monsters/new")
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkMonsterNameConflict(monster.Name, 0); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect("/admin/monsters/new")
|
||||
return
|
||||
}
|
||||
|
||||
if err := monster.Insert(); err != nil {
|
||||
sess.SetFlash("error", "Failed to create monster")
|
||||
ctx.Redirect("/admin/monsters/new")
|
||||
return
|
||||
}
|
||||
|
||||
sess.SetFlash("success", fmt.Sprintf("Monster %s created successfully", monster.Name))
|
||||
ctx.Redirect("/admin/monsters")
|
||||
}
|
||||
|
||||
func adminMonsterEdit(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
id := ctx.Param("id").Int()
|
||||
|
||||
monster, err := monsters.Find(id)
|
||||
if err != nil {
|
||||
sess.SetFlash("error", fmt.Sprintf("Monster %d not found", id))
|
||||
ctx.Redirect("/admin/monsters")
|
||||
return
|
||||
}
|
||||
|
||||
components.RenderAdminPage(ctx, fmt.Sprintf("Edit Monster: %s", monster.Name), "admin/monsters/edit.html", map[string]any{
|
||||
"monster": monster,
|
||||
})
|
||||
}
|
||||
|
||||
func adminMonsterUpdate(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
id := ctx.Param("id").Int()
|
||||
|
||||
monster, err := monsters.Find(id)
|
||||
if err != nil {
|
||||
sess.SetFlash("error", fmt.Sprintf("Monster %d not found", id))
|
||||
ctx.Redirect("/admin/monsters")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if delete was requested
|
||||
if ctx.Form("delete").String() == "1" {
|
||||
if err := monster.Delete(); err != nil {
|
||||
sess.SetFlash("error", "Failed to delete monster")
|
||||
ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id))
|
||||
return
|
||||
}
|
||||
sess.SetFlash("success", fmt.Sprintf("Monster %s deleted successfully", monster.Name))
|
||||
ctx.Redirect("/admin/monsters")
|
||||
return
|
||||
}
|
||||
|
||||
if err := populateMonsterFromForm(ctx, monster); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
if err := monster.Validate(); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkMonsterNameConflict(monster.Name, monster.ID); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
fields := map[string]any{
|
||||
"name": monster.Name,
|
||||
"level": monster.Level,
|
||||
"max_hp": monster.MaxHP,
|
||||
"max_dmg": monster.MaxDmg,
|
||||
"armor": monster.Armor,
|
||||
"max_exp": monster.MaxExp,
|
||||
"max_gold": monster.MaxGold,
|
||||
"immune": monster.Immune,
|
||||
}
|
||||
|
||||
if err := database.Update("monsters", fields, "id", id); err != nil {
|
||||
sess.SetFlash("error", "Failed to update monster")
|
||||
ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
sess.SetFlash("success", fmt.Sprintf("Monster %s updated successfully", monster.Name))
|
||||
ctx.Redirect("/admin/monsters")
|
||||
}
|
||||
|
||||
func populateMonsterFromForm(ctx sushi.Ctx, monster *monsters.Monster) error {
|
||||
monster.Name = strings.TrimSpace(ctx.Form("name").String())
|
||||
monster.Level = ctx.Form("level").Int()
|
||||
monster.MaxHP = ctx.Form("max_hp").Int()
|
||||
monster.MaxDmg = ctx.Form("max_dmg").Int()
|
||||
monster.Armor = ctx.Form("armor").Int()
|
||||
monster.MaxExp = ctx.Form("max_exp").Int()
|
||||
monster.MaxGold = ctx.Form("max_gold").Int()
|
||||
monster.Immune = ctx.Form("immune").Int()
|
||||
|
||||
if monster.Name == "" {
|
||||
return fmt.Errorf("monster name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMonsterNameConflict(name string, excludeID int) error {
|
||||
existingMonster, err := monsters.ByName(name)
|
||||
if err != nil {
|
||||
return nil // No conflict if no monster found or database error
|
||||
}
|
||||
|
||||
if existingMonster != nil && existingMonster.ID != excludeID {
|
||||
return fmt.Errorf("a monster with the name '%s' already exists", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bToMb(b uint64) uint64 {
|
||||
return b / 1024 / 1024
|
||||
}
|
||||
|
52
templates/admin/monsters/edit.html
Normal file
52
templates/admin/monsters/edit.html
Normal file
@ -0,0 +1,52 @@
|
||||
{include "admin/layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>{if monster.ID}Edit Monster: {monster.Name}{else}Add New Monster{/if}</h1>
|
||||
|
||||
<form class="standard" method="post">
|
||||
{csrf}
|
||||
<div>
|
||||
<label for="name">Monster Name:</label>
|
||||
<input type="text" name="name" id="name" value="{monster.Name}" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="level">Level:</label>
|
||||
<input type="number" name="level" id="level" value="{monster.Level}" min="1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="max_hp">Max HP:</label>
|
||||
<input type="number" name="max_hp" id="max_hp" value="{monster.MaxHP}" min="1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="max_dmg">Max Damage:</label>
|
||||
<input type="number" name="max_dmg" id="max_dmg" value="{monster.MaxDmg}" min="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="armor">Armor:</label>
|
||||
<input type="number" name="armor" id="armor" value="{monster.Armor}" min="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="max_exp">Max Experience:</label>
|
||||
<input type="number" name="max_exp" id="max_exp" value="{monster.MaxExp}" min="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="max_gold">Max Gold:</label>
|
||||
<input type="number" name="max_gold" id="max_gold" value="{monster.MaxGold}" min="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="immune">Immunity:</label>
|
||||
<select name="immune" id="immune" required>
|
||||
<option value="0"{if monster.Immune == 0} selected{/if}>None</option>
|
||||
<option value="1"{if monster.Immune == 1} selected{/if}>Hurt Spells</option>
|
||||
<option value="2"{if monster.Immune == 2} selected{/if}>Sleep Spells</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/monsters"><button type="button" class="btn">Cancel</button></a>
|
||||
<button type="submit" class="btn btn-primary">{if monster.ID}Update{else}Create{/if} Monster</button>
|
||||
{if monster.ID}
|
||||
<button type="submit" name="delete" value="1" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this monster?')">Delete Monster</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/block}
|
66
templates/admin/monsters/index.html
Normal file
66
templates/admin/monsters/index.html
Normal file
@ -0,0 +1,66 @@
|
||||
{include "admin/layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>Monster Management</h1>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="/admin/monsters/new" class="btn btn-primary">Add New Monster</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Total monsters: {#monsters} | Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
|
||||
{if #monsters > 0}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Level</th>
|
||||
<th>HP</th>
|
||||
<th>Damage</th>
|
||||
<th>Armor</th>
|
||||
<th>Exp</th>
|
||||
<th>Gold</th>
|
||||
<th>Immunity</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{for monster in monsters}
|
||||
<tr>
|
||||
<td>{monster.ID}</td>
|
||||
<td>{monster.Name}</td>
|
||||
<td>{monster.Level}</td>
|
||||
<td>{monster.MaxHP}</td>
|
||||
<td>{monster.MaxDmg}</td>
|
||||
<td>{monster.Armor}</td>
|
||||
<td>{monster.MaxExp}</td>
|
||||
<td>{monster.MaxGold}</td>
|
||||
<td>{monster.ImmunityName}</td>
|
||||
<td>
|
||||
<a href="/admin/monsters/{monster.ID}">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/for}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{if totalPages > 1}
|
||||
<div class="pagination">
|
||||
{if hasPrev}
|
||||
<a href="/admin/monsters?page={currentPage - 1}">← Previous</a>
|
||||
{/if}
|
||||
{if hasNext}
|
||||
<a href="/admin/monsters?page={currentPage + 1}">Next →</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{else}
|
||||
<div>
|
||||
No monsters found.
|
||||
</div>
|
||||
{/if}
|
||||
{/block}
|
Loading…
x
Reference in New Issue
Block a user