work template engine, implement item purchase and equip
This commit is contained in:
parent
90923cbfe7
commit
53f778c323
@ -197,7 +197,7 @@ button.btn {
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
background-color: #F2994A;
|
||||
background-image: url("/assets/images/overlay.png"), linear-gradient(to bottom, #F2C94C, #F2994A);
|
||||
border-color: #F2994A;
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 7.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 94 B |
Binary file not shown.
Before Width: | Height: | Size: 94 B |
Binary file not shown.
Before Width: | Height: | Size: 94 B |
56
internal/actions/user_item.go
Normal file
56
internal/actions/user_item.go
Normal file
@ -0,0 +1,56 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"dk/internal/items"
|
||||
"dk/internal/users"
|
||||
)
|
||||
|
||||
// UserEquipItem equips a given item onto a user. This overwrites any
|
||||
// previously equipped item in the slot. Does not save.
|
||||
func UserEquipItem(user *users.User, item *items.Item) {
|
||||
slotInUse := false
|
||||
if item.Type == items.TypeWeapon && user.WeaponID != 0 {
|
||||
slotInUse = true
|
||||
}
|
||||
if item.Type == items.TypeArmor && user.ArmorID != 0 {
|
||||
slotInUse = true
|
||||
}
|
||||
if item.Type == items.TypeShield && user.ShieldID != 0 {
|
||||
slotInUse = true
|
||||
}
|
||||
|
||||
var oldItem *items.Item
|
||||
if slotInUse && item.Type == items.TypeWeapon {
|
||||
oldItem, _ = items.Find(user.WeaponID)
|
||||
} else if slotInUse && item.Type == items.TypeArmor {
|
||||
oldItem, _ = items.Find(user.ArmorID)
|
||||
} else if slotInUse && item.Type == items.TypeShield {
|
||||
oldItem, _ = items.Find(user.ShieldID)
|
||||
}
|
||||
|
||||
if oldItem != nil {
|
||||
switch oldItem.Type {
|
||||
case items.TypeWeapon:
|
||||
user.Set("Attack", user.Attack-oldItem.Att)
|
||||
case items.TypeArmor:
|
||||
user.Set("Defense", user.Defense-oldItem.Att)
|
||||
case items.TypeShield:
|
||||
user.Set("Defense", user.Defense-oldItem.Att)
|
||||
}
|
||||
}
|
||||
|
||||
switch item.Type {
|
||||
case items.TypeWeapon:
|
||||
user.Set("Attack", user.Attack+item.Att)
|
||||
user.Set("WeaponID", item.ID)
|
||||
user.Set("WeaponName", item.Name)
|
||||
case items.TypeArmor:
|
||||
user.Set("Defense", user.Defense+item.Att)
|
||||
user.Set("ArmorID", item.ID)
|
||||
user.Set("ArmorName", item.Name)
|
||||
case items.TypeShield:
|
||||
user.Set("Defense", user.Defense+item.Att)
|
||||
user.Set("ShieldID", item.ID)
|
||||
user.Set("ShieldName", item.Name)
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"dk/internal/actions"
|
||||
"dk/internal/auth"
|
||||
"dk/internal/helpers"
|
||||
"dk/internal/items"
|
||||
@ -9,6 +10,8 @@ import (
|
||||
"dk/internal/template/components"
|
||||
"dk/internal/towns"
|
||||
"dk/internal/users"
|
||||
"slices"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func RegisterTownRoutes(r *router.Router) {
|
||||
@ -20,6 +23,7 @@ func RegisterTownRoutes(r *router.Router) {
|
||||
group.Get("/inn", showInn)
|
||||
group.Get("/shop", showShop)
|
||||
group.WithMiddleware(middleware.CSRF(auth.Manager)).Post("/inn", rest)
|
||||
group.Get("/shop/buy/:id", buyItem)
|
||||
}
|
||||
|
||||
func showTown(ctx router.Ctx, _ []string) {
|
||||
@ -93,3 +97,39 @@ func showShop(ctx router.Ctx, _ []string) {
|
||||
"error_message": errorHTML,
|
||||
})
|
||||
}
|
||||
|
||||
func buyItem(ctx router.Ctx, params []string) {
|
||||
id, err := strconv.Atoi(params[0])
|
||||
if err != nil {
|
||||
auth.SetFlashMessage(ctx, "error", "Error purchasing item; "+err.Error())
|
||||
ctx.Redirect("/town/shop", 302)
|
||||
return
|
||||
}
|
||||
|
||||
town := ctx.UserValue("town").(*towns.Town)
|
||||
if !slices.Contains(town.GetShopItems(), id) {
|
||||
auth.SetFlashMessage(ctx, "error", "The item doesn't exist in this shop.")
|
||||
ctx.Redirect("/town/shop", 302)
|
||||
return
|
||||
}
|
||||
|
||||
item, err := items.Find(id)
|
||||
if err != nil {
|
||||
auth.SetFlashMessage(ctx, "error", "Error purchasing item; "+err.Error())
|
||||
ctx.Redirect("/town/shop", 302)
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.UserValue("user").(*users.User)
|
||||
if user.Gold < item.Value {
|
||||
auth.SetFlashMessage(ctx, "error", "You don't have enough gold to buy "+item.Name)
|
||||
ctx.Redirect("/town/shop", 302)
|
||||
return
|
||||
}
|
||||
|
||||
user.Set("Gold", user.Gold-item.Value)
|
||||
actions.UserEquipItem(user, item)
|
||||
user.Save()
|
||||
|
||||
ctx.Redirect("/town/shop", 302)
|
||||
}
|
||||
|
100
internal/template/cache.go
Normal file
100
internal/template/cache.go
Normal file
@ -0,0 +1,100 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var Cache *TemplateCache
|
||||
|
||||
type TemplateCache struct {
|
||||
mu sync.RWMutex
|
||||
templates map[string]*Template
|
||||
basePath string
|
||||
}
|
||||
|
||||
func NewCache(basePath string) *TemplateCache {
|
||||
if basePath == "" {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
basePath = "."
|
||||
} else {
|
||||
basePath = filepath.Dir(exe)
|
||||
}
|
||||
}
|
||||
|
||||
return &TemplateCache{
|
||||
templates: make(map[string]*Template),
|
||||
basePath: basePath,
|
||||
}
|
||||
}
|
||||
|
||||
func InitializeCache(basePath string) {
|
||||
Cache = NewCache(basePath)
|
||||
}
|
||||
|
||||
func (c *TemplateCache) Load(name string) (*Template, error) {
|
||||
c.mu.RLock()
|
||||
tmpl, exists := c.templates[name]
|
||||
c.mu.RUnlock()
|
||||
|
||||
if exists {
|
||||
if err := c.checkAndReload(tmpl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
return c.loadFromFile(name)
|
||||
}
|
||||
|
||||
func (c *TemplateCache) loadFromFile(name string) (*Template, error) {
|
||||
filePath := filepath.Join(c.basePath, "templates", name)
|
||||
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("template file not found: %s", name)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read template: %w", err)
|
||||
}
|
||||
|
||||
tmpl := &Template{
|
||||
name: name,
|
||||
content: string(content),
|
||||
modTime: info.ModTime(),
|
||||
filePath: filePath,
|
||||
cache: c,
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.templates[name] = tmpl
|
||||
c.mu.Unlock()
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func (c *TemplateCache) checkAndReload(tmpl *Template) error {
|
||||
info, err := os.Stat(tmpl.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.ModTime().After(tmpl.modTime) {
|
||||
content, err := os.ReadFile(tmpl.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
tmpl.content = string(content)
|
||||
tmpl.modTime = info.ModTime()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,314 +0,0 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewCache(t *testing.T) {
|
||||
cache := NewCache("")
|
||||
if cache == nil {
|
||||
t.Fatal("NewCache returned nil")
|
||||
}
|
||||
if cache.templates == nil {
|
||||
t.Fatal("templates map not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPositionalReplacement(t *testing.T) {
|
||||
tmpl := &Template{
|
||||
name: "test",
|
||||
content: "Hello {0}, you are {1} years old!",
|
||||
}
|
||||
|
||||
result := tmpl.RenderPositional("Alice", 25)
|
||||
expected := "Hello Alice, you are 25 years old!"
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Expected %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamedReplacement(t *testing.T) {
|
||||
tmpl := &Template{
|
||||
name: "test",
|
||||
content: "Hello {name}, you are {age} years old!",
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"name": "Bob",
|
||||
"age": 30,
|
||||
}
|
||||
|
||||
result := tmpl.RenderNamed(data)
|
||||
expected := "Hello Bob, you are 30 years old!"
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Expected %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDotNotationReplacement(t *testing.T) {
|
||||
tmpl := &Template{
|
||||
name: "test",
|
||||
content: "User: {user.name}, Email: {user.contact.email}",
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"user": map[string]any{
|
||||
"name": "Charlie",
|
||||
"contact": map[string]any{
|
||||
"email": "charlie@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := tmpl.RenderNamed(data)
|
||||
expected := "User: Charlie, Email: charlie@example.com"
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Expected %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateLoadingAndCaching(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "template_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
templatesDir := filepath.Join(tmpDir, "templates")
|
||||
err = os.MkdirAll(templatesDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
templateFile := filepath.Join(templatesDir, "test.html")
|
||||
content := "Hello {name}!"
|
||||
err = os.WriteFile(templateFile, []byte(content), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cache := NewCache(tmpDir)
|
||||
|
||||
tmpl, err := cache.Load("test.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if tmpl.content != content {
|
||||
t.Errorf("Expected content %q, got %q", content, tmpl.content)
|
||||
}
|
||||
|
||||
tmpl2, err := cache.Load("test.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if tmpl != tmpl2 {
|
||||
t.Error("Template should be cached and return same instance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateReloading(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "template_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
templatesDir := filepath.Join(tmpDir, "templates")
|
||||
err = os.MkdirAll(templatesDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
templateFile := filepath.Join(templatesDir, "test.html")
|
||||
content1 := "Hello {name}!"
|
||||
err = os.WriteFile(templateFile, []byte(content1), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cache := NewCache(tmpDir)
|
||||
|
||||
tmpl, err := cache.Load("test.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if tmpl.content != content1 {
|
||||
t.Errorf("Expected content %q, got %q", content1, tmpl.content)
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
content2 := "Hi {name}, welcome!"
|
||||
err = os.WriteFile(templateFile, []byte(content2), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tmpl2, err := cache.Load("test.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if tmpl2.content != content2 {
|
||||
t.Errorf("Expected reloaded content %q, got %q", content2, tmpl2.content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNestedValue(t *testing.T) {
|
||||
tmpl := &Template{}
|
||||
|
||||
data := map[string]any{
|
||||
"level1": map[string]any{
|
||||
"level2": map[string]any{
|
||||
"value": "found",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := tmpl.getNestedValue(data, "level1.level2.value")
|
||||
if result != "found" {
|
||||
t.Errorf("Expected 'found', got %v", result)
|
||||
}
|
||||
|
||||
result = tmpl.getNestedValue(data, "level1.nonexistent")
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for nonexistent path, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMixedReplacementTypes(t *testing.T) {
|
||||
tmpl := &Template{
|
||||
name: "test",
|
||||
content: "Hello {name}, you have {count} {items.type}s!",
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"name": "Dave",
|
||||
"count": 5,
|
||||
"items": map[string]any{
|
||||
"type": "apple",
|
||||
},
|
||||
}
|
||||
|
||||
result := tmpl.RenderNamed(data)
|
||||
expected := "Hello Dave, you have 5 apples!"
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Expected %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeSupport(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "template_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
templatesDir := filepath.Join(tmpDir, "templates")
|
||||
err = os.MkdirAll(templatesDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create main template with include
|
||||
mainTemplate := `<html><head>{include "header.html"}</head><body>Hello {name}!</body></html>`
|
||||
err = os.WriteFile(filepath.Join(templatesDir, "main.html"), []byte(mainTemplate), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create included template
|
||||
headerTemplate := `<title>{title}</title><meta charset="utf-8">`
|
||||
err = os.WriteFile(filepath.Join(templatesDir, "header.html"), []byte(headerTemplate), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cache := NewCache(tmpDir)
|
||||
tmpl, err := cache.Load("main.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"name": "Alice",
|
||||
"title": "Welcome",
|
||||
}
|
||||
|
||||
result := tmpl.RenderNamed(data)
|
||||
expected := "<html><head><title>Welcome</title><meta charset=\"utf-8\"></head><body>Hello Alice!</body></html>"
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Expected %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlocksAndYield(t *testing.T) {
|
||||
tmpl := &Template{
|
||||
name: "test",
|
||||
content: `{block "content"}Default content{/block}<main>{yield content}</main>`,
|
||||
}
|
||||
|
||||
result := tmpl.RenderNamed(map[string]any{})
|
||||
expected := "<main>Default content</main>"
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Expected %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateInheritance(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "template_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
templatesDir := filepath.Join(tmpDir, "templates")
|
||||
err = os.MkdirAll(templatesDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Layout template
|
||||
layoutTemplate := `<!DOCTYPE html><html><head><title>{title}</title></head><body>{yield content}</body></html>`
|
||||
err = os.WriteFile(filepath.Join(templatesDir, "layout.html"), []byte(layoutTemplate), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Page template that extends layout
|
||||
pageTemplate := `{include "layout.html"}{block "content"}<h1>Welcome {name}!</h1><p>This is the content block.</p>{/block}`
|
||||
err = os.WriteFile(filepath.Join(templatesDir, "page.html"), []byte(pageTemplate), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cache := NewCache(tmpDir)
|
||||
tmpl, err := cache.Load("page.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"title": "Test Page",
|
||||
"name": "Bob",
|
||||
}
|
||||
|
||||
result := tmpl.RenderNamed(data)
|
||||
expected := `<!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Welcome Bob!</h1><p>This is the content block.</p></body></html>`
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Expected %q, got %q", expected, result)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user