work template engine, implement item purchase and equip

This commit is contained in:
Sky Johnson 2025-08-13 10:17:52 -05:00
parent 90923cbfe7
commit 53f778c323
11 changed files with 611 additions and 892 deletions

View File

@ -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

View 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)
}
}

View File

@ -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
View 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

View File

@ -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)
}
}