From 2b86e9fa79640629709a7473ab099e571f5faaa8 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Tue, 12 Aug 2025 20:00:49 -0500 Subject: [PATCH] rework all models to new paradigm, fix ordered map to work with any interface correctly --- internal/babble/babble.go | 146 ++++++++++---------------- internal/control/control.go | 169 ++++++++++++------------------ internal/drops/drops.go | 110 +++++++------------- internal/forum/forum.go | 175 ++++++++++++-------------------- internal/helpers/ordered_map.go | 9 +- internal/items/items.go | 122 ++++++++-------------- internal/monsters/monsters.go | 144 ++++++++++---------------- internal/news/news.go | 141 ++++++++++--------------- internal/routes/town.go | 23 +++++ internal/spells/spells.go | 138 +++++++++---------------- internal/towns/towns.go | 172 ++++++++++++------------------- internal/users/users.go | 68 ------------- templates/leftside.html | 8 +- templates/rightside.html | 4 +- templates/town/shop.html | 31 ++++++ 15 files changed, 545 insertions(+), 915 deletions(-) create mode 100644 templates/town/shop.html diff --git a/internal/babble/babble.go b/internal/babble/babble.go index 4b3e089..efdd25b 100644 --- a/internal/babble/babble.go +++ b/internal/babble/babble.go @@ -13,13 +13,39 @@ import ( // Babble represents a global chat message in the database type Babble struct { + database.BaseModel + ID int `db:"id" json:"id"` Posted int64 `db:"posted" json:"posted"` Author string `db:"author" json:"author"` Babble string `db:"babble" json:"babble"` } -// New creates a new Babble with sensible defaults +func (b *Babble) GetTableName() string { + return "babble" +} + +func (b *Babble) GetID() int { + return b.ID +} + +func (b *Babble) SetID(id int) { + b.ID = id +} + +func (b *Babble) Set(field string, value any) error { + return database.Set(b, field, value) +} + +func (b *Babble) Save() error { + return database.Save(b) +} + +func (b *Babble) Delete() error { + return database.Delete(b) +} + +// Creates a new Babble with sensible defaults func New() *Babble { return &Babble{ Posted: time.Now().Unix(), @@ -30,19 +56,19 @@ func New() *Babble { var babbleScanner = scanner.New[Babble]() -// babbleColumns returns the column list for babble queries +// Returns the column list for babble queries func babbleColumns() string { return babbleScanner.Columns() } -// scanBabble populates a Babble struct using the fast scanner +// Populates a Babble struct using the fast scanner func scanBabble(stmt *sqlite.Stmt) *Babble { babble := &Babble{} babbleScanner.Scan(stmt, babble) return babble } -// Find retrieves a babble message by ID +// Retrieves a babble message by ID func Find(id int) (*Babble, error) { var babble *Babble @@ -64,7 +90,7 @@ func Find(id int) (*Babble, error) { return babble, nil } -// All retrieves all babble messages ordered by posted time (newest first) +// Retrieves all babble messages ordered by posted time (newest first) func All() ([]*Babble, error) { var babbles []*Babble @@ -83,7 +109,7 @@ func All() ([]*Babble, error) { return babbles, nil } -// ByAuthor retrieves babble messages by a specific author +// Retrieves babble messages by a specific author func ByAuthor(author string) ([]*Babble, error) { var babbles []*Babble @@ -102,7 +128,7 @@ func ByAuthor(author string) ([]*Babble, error) { return babbles, nil } -// Recent retrieves the most recent babble messages (limited by count) +// Retrieves the most recent babble messages (limited by count) func Recent(limit int) ([]*Babble, error) { var babbles []*Babble @@ -121,7 +147,7 @@ func Recent(limit int) ([]*Babble, error) { return babbles, nil } -// Since retrieves babble messages since a specific timestamp +// Retrieves babble messages since a specific timestamp func Since(since int64) ([]*Babble, error) { var babbles []*Babble @@ -140,7 +166,7 @@ func Since(since int64) ([]*Babble, error) { return babbles, nil } -// Between retrieves babble messages between two timestamps (inclusive) +// Retrieves babble messages between two timestamps (inclusive) func Between(start, end int64) ([]*Babble, error) { var babbles []*Babble @@ -159,7 +185,7 @@ func Between(start, end int64) ([]*Babble, error) { return babbles, nil } -// Search retrieves babble messages containing the search term (case-insensitive) +// Retrieves babble messages containing the search term (case-insensitive) func Search(term string) ([]*Babble, error) { var babbles []*Babble @@ -179,7 +205,7 @@ func Search(term string) ([]*Babble, error) { return babbles, nil } -// RecentByAuthor retrieves recent messages from a specific author +// Retrieves recent messages from a specific author func RecentByAuthor(author string, limit int) ([]*Babble, error) { var babbles []*Babble @@ -198,82 +224,39 @@ func RecentByAuthor(author string, limit int) ([]*Babble, error) { return babbles, nil } -// Save updates an existing babble message in the database -func (b *Babble) Save() error { - if b.ID == 0 { - return fmt.Errorf("cannot save babble without ID") - } - - query := `UPDATE babble SET posted = ?, author = ?, babble = ? WHERE id = ?` - return database.Exec(query, b.Posted, b.Author, b.Babble, b.ID) -} - -// Insert saves a new babble to the database and sets the ID +// Saves a new babble to the database and sets the ID func (b *Babble) Insert() error { - if b.ID != 0 { - return fmt.Errorf("babble already has ID %d, use Save() to update", b.ID) - } - - // Use a transaction to ensure we can get the ID - err := database.Transaction(func(tx *database.Tx) error { - query := `INSERT INTO babble (posted, author, babble) VALUES (?, ?, ?)` - - if err := tx.Exec(query, b.Posted, b.Author, b.Babble); err != nil { - return fmt.Errorf("failed to insert babble: %w", err) - } - - // Get the last insert ID - var id int - err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { - id = stmt.ColumnInt(0) - return nil - }) - if err != nil { - return fmt.Errorf("failed to get insert ID: %w", err) - } - - b.ID = id - return nil - }) - - return err + columns := `posted, author, babble` + values := []any{b.Posted, b.Author, b.Babble} + return database.Insert(b, columns, values...) } -// Delete removes the babble message from the database -func (b *Babble) Delete() error { - if b.ID == 0 { - return fmt.Errorf("cannot delete babble without ID") - } - - return database.Exec("DELETE FROM babble WHERE id = ?", b.ID) -} - -// PostedTime returns the posted timestamp as a time.Time +// Returns the posted timestamp as a time.Time func (b *Babble) PostedTime() time.Time { return time.Unix(b.Posted, 0) } -// SetPostedTime sets the posted timestamp from a time.Time +// Sets the posted timestamp from a time.Time func (b *Babble) SetPostedTime(t time.Time) { - b.Posted = t.Unix() + b.Set("Posted", t.Unix()) } -// IsRecent returns true if the babble message was posted within the last hour +// Returns true if the babble message was posted within the last hour func (b *Babble) IsRecent() bool { return time.Since(b.PostedTime()) < time.Hour } -// Age returns how long ago the babble message was posted +// Returns how long ago the babble message was posted func (b *Babble) Age() time.Duration { return time.Since(b.PostedTime()) } -// IsAuthor returns true if the given username is the author of this babble message +// Returns true if the given username is the author of this babble message func (b *Babble) IsAuthor(username string) bool { return strings.EqualFold(b.Author, username) } -// Preview returns a truncated version of the babble for previews +// Returns a truncated version of the babble for previews func (b *Babble) Preview(maxLength int) string { if len(b.Babble) <= maxLength { return b.Babble @@ -286,7 +269,7 @@ func (b *Babble) Preview(maxLength int) string { return b.Babble[:maxLength-3] + "..." } -// WordCount returns the number of words in the babble message +// Returns the number of words in the babble message func (b *Babble) WordCount() int { if b.Babble == "" { return 0 @@ -314,27 +297,27 @@ func (b *Babble) WordCount() int { return words } -// Length returns the character length of the babble message +// Returns the character length of the babble message func (b *Babble) Length() int { return len(b.Babble) } -// Contains returns true if the babble message contains the given term (case-insensitive) +// Returns true if the babble message contains the given term (case-insensitive) func (b *Babble) Contains(term string) bool { return strings.Contains(strings.ToLower(b.Babble), strings.ToLower(term)) } -// IsEmpty returns true if the babble message is empty or whitespace-only +// Returns true if the babble message is empty or whitespace-only func (b *Babble) IsEmpty() bool { return strings.TrimSpace(b.Babble) == "" } -// IsLongMessage returns true if the message exceeds the typical chat length +// Returns true if the message exceeds the typical chat length func (b *Babble) IsLongMessage(threshold int) bool { return b.Length() > threshold } -// GetMentions returns a slice of usernames mentioned in the message (starting with @) +// Returns a slice of usernames mentioned in the message (starting with @) func (b *Babble) GetMentions() []string { words := strings.Fields(b.Babble) var mentions []string @@ -352,7 +335,7 @@ func (b *Babble) GetMentions() []string { return mentions } -// HasMention returns true if the message mentions the given username +// Returns true if the message mentions the given username func (b *Babble) HasMention(username string) bool { mentions := b.GetMentions() for _, mention := range mentions { @@ -362,22 +345,3 @@ func (b *Babble) HasMention(username string) bool { } return false } - -// ToMap converts the babble to a map for efficient template rendering -func (b *Babble) ToMap() map[string]any { - return map[string]any{ - "ID": b.ID, - "Posted": b.Posted, - "Author": b.Author, - "Babble": b.Babble, - - // Computed values - "PostedTime": b.PostedTime(), - "IsRecent": b.IsRecent(), - "Age": b.Age(), - "WordCount": b.WordCount(), - "Length": b.Length(), - "IsEmpty": b.IsEmpty(), - "Mentions": b.GetMentions(), - } -} diff --git a/internal/control/control.go b/internal/control/control.go index cd28099..dfd1272 100644 --- a/internal/control/control.go +++ b/internal/control/control.go @@ -11,6 +11,8 @@ import ( // Control represents the game control settings in the database type Control struct { + database.BaseModel + ID int `db:"id" json:"id"` WorldSize int `db:"world_size" json:"world_size"` Open int `db:"open" json:"open"` @@ -20,7 +22,31 @@ type Control struct { Class3Name string `db:"class_3_name" json:"class_3_name"` } -// New creates a new Control with sensible defaults +func (c *Control) GetTableName() string { + return "control" +} + +func (c *Control) GetID() int { + return c.ID +} + +func (c *Control) SetID(id int) { + c.ID = id +} + +func (c *Control) Set(field string, value any) error { + return database.Set(c, field, value) +} + +func (c *Control) Save() error { + return database.Save(c) +} + +func (c *Control) Delete() error { + return database.Delete(c) +} + +// Creates a new Control with sensible defaults func New() *Control { return &Control{ WorldSize: 200, // Default world size @@ -34,19 +60,19 @@ func New() *Control { var controlScanner = scanner.New[Control]() -// controlColumns returns the column list for control queries +// Returns the column list for control queries func controlColumns() string { return controlScanner.Columns() } -// scanControl populates a Control struct using the fast scanner +// Populates a Control struct using the fast scanner func scanControl(stmt *sqlite.Stmt) *Control { control := &Control{} controlScanner.Scan(stmt, control) return control } -// Find retrieves the control record by ID (typically only ID 1 exists) +// Retrieves the control record by ID (typically only ID 1 exists) func Find(id int) (*Control, error) { var control *Control @@ -68,86 +94,43 @@ func Find(id int) (*Control, error) { return control, nil } -// Get retrieves the main control record (ID 1) +// Retrieves the main control record (ID 1) func Get() (*Control, error) { return Find(1) } -// Save updates the control record in the database -func (c *Control) Save() error { - if c.ID == 0 { - return fmt.Errorf("cannot save control without ID") - } - - query := `UPDATE control SET world_size = ?, open = ?, admin_email = ?, class_1_name = ?, class_2_name = ?, class_3_name = ? WHERE id = ?` - return database.Exec(query, c.WorldSize, c.Open, c.AdminEmail, c.Class1Name, c.Class2Name, c.Class3Name, c.ID) -} - -// Insert saves a new control to the database and sets the ID +// Saves a new control to the database and sets the ID func (c *Control) Insert() error { - if c.ID != 0 { - return fmt.Errorf("control already has ID %d, use Save() to update", c.ID) - } - - // Use a transaction to ensure we can get the ID - err := database.Transaction(func(tx *database.Tx) error { - query := `INSERT INTO control (world_size, open, admin_email, class_1_name, class_2_name, class_3_name) VALUES (?, ?, ?, ?, ?, ?)` - - if err := tx.Exec(query, c.WorldSize, c.Open, c.AdminEmail, c.Class1Name, c.Class2Name, c.Class3Name); err != nil { - return fmt.Errorf("failed to insert control: %w", err) - } - - // Get the last insert ID - var id int - err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { - id = stmt.ColumnInt(0) - return nil - }) - if err != nil { - return fmt.Errorf("failed to get insert ID: %w", err) - } - - c.ID = id - return nil - }) - - return err + columns := `world_size, open, admin_email, class_1_name, class_2_name, class_3_name` + values := []any{c.WorldSize, c.Open, c.AdminEmail, c.Class1Name, c.Class2Name, c.Class3Name} + return database.Insert(c, columns, values...) } -// Delete removes the control record from the database -func (c *Control) Delete() error { - if c.ID == 0 { - return fmt.Errorf("cannot delete control without ID") - } - - return database.Exec("DELETE FROM control WHERE id = ?", c.ID) -} - -// IsOpen returns true if the game world is open for new players +// Returns true if the game world is open for new players func (c *Control) IsOpen() bool { return c.Open == 1 } -// SetOpen sets whether the game world is open for new players +// Sets whether the game world is open for new players func (c *Control) SetOpen(open bool) { if open { - c.Open = 1 + c.Set("Open", 1) } else { - c.Open = 0 + c.Set("Open", 0) } } -// Close closes the game world to new players +// Closes the game world to new players func (c *Control) Close() { - c.Open = 0 + c.Set("Open", 0) } -// OpenWorld opens the game world to new players +// Opens the game world to new players func (c *Control) OpenWorld() { - c.Open = 1 + c.Set("Open", 1) } -// GetClassNames returns all class names as a slice +// Returns all class names as a slice func (c *Control) GetClassNames() []string { classes := make([]string, 0, 3) if c.Class1Name != "" { @@ -162,26 +145,26 @@ func (c *Control) GetClassNames() []string { return classes } -// SetClassNames sets all class names from a slice +// Sets all class names from a slice func (c *Control) SetClassNames(classes []string) { // Reset all class names - c.Class1Name = "" - c.Class2Name = "" - c.Class3Name = "" + c.Set("Class1Name", "") + c.Set("Class2Name", "") + c.Set("Class3Name", "") // Set provided class names if len(classes) > 0 { - c.Class1Name = classes[0] + c.Set("Class1Name", classes[0]) } if len(classes) > 1 { - c.Class2Name = classes[1] + c.Set("Class2Name", classes[1]) } if len(classes) > 2 { - c.Class3Name = classes[2] + c.Set("Class3Name", classes[2]) } } -// GetClassName returns the name of a specific class (1-3) +// Returns the name of a specific class (1-3) func (c *Control) GetClassName(classNum int) string { switch classNum { case 1: @@ -195,24 +178,24 @@ func (c *Control) GetClassName(classNum int) string { } } -// SetClassName sets the name of a specific class (1-3) +// Sets the name of a specific class (1-3) func (c *Control) SetClassName(classNum int, name string) bool { switch classNum { case 1: - c.Class1Name = name + c.Set("Class1Name", name) return true case 2: - c.Class2Name = name + c.Set("Class2Name", name) return true case 3: - c.Class3Name = name + c.Set("Class3Name", name) return true default: return false } } -// IsValidClassName returns true if the given name matches one of the configured classes +// Returns true if the given name matches one of the configured classes func (c *Control) IsValidClassName(name string) bool { if name == "" { return false @@ -220,7 +203,7 @@ func (c *Control) IsValidClassName(name string) bool { return name == c.Class1Name || name == c.Class2Name || name == c.Class3Name } -// GetClassNumber returns the class number (1-3) for a given class name, or 0 if not found +// Returns the class number (1-3) for a given class name, or 0 if not found func (c *Control) GetClassNumber(name string) int { if name == c.Class1Name && name != "" { return 1 @@ -234,55 +217,29 @@ func (c *Control) GetClassNumber(name string) int { return 0 } -// HasAdminEmail returns true if an admin email is configured +// Returns true if an admin email is configured func (c *Control) HasAdminEmail() bool { return c.AdminEmail != "" } -// IsWorldSizeValid returns true if the world size is within reasonable bounds +// Returns true if the world size is within reasonable bounds func (c *Control) IsWorldSizeValid() bool { return c.WorldSize > 0 && c.WorldSize <= 10000 } -// GetWorldRadius returns the world radius (half the world size) +// Returns the world radius (half the world size) func (c *Control) GetWorldRadius() int { return c.WorldSize / 2 } -// IsWithinWorldBounds returns true if the given coordinates are within world bounds +// Returns true if the given coordinates are within world bounds func (c *Control) IsWithinWorldBounds(x, y int) bool { radius := c.GetWorldRadius() return x >= -radius && x <= radius && y >= -radius && y <= radius } -// GetWorldBounds returns the minimum and maximum coordinates for the world +// Returns the minimum and maximum coordinates for the world func (c *Control) GetWorldBounds() (minX, minY, maxX, maxY int) { radius := c.GetWorldRadius() return -radius, -radius, radius, radius } - -// ToMap converts the control to a map for efficient template rendering -func (c *Control) ToMap() map[string]any { - return map[string]any{ - "ID": c.ID, - "WorldSize": c.WorldSize, - "Open": c.Open, - "AdminEmail": c.AdminEmail, - "Class1Name": c.Class1Name, - "Class2Name": c.Class2Name, - "Class3Name": c.Class3Name, - - // Computed values - "IsOpen": c.IsOpen(), - "ClassNames": c.GetClassNames(), - "HasAdminEmail": c.HasAdminEmail(), - "IsWorldSizeValid": c.IsWorldSizeValid(), - "WorldRadius": c.GetWorldRadius(), - "WorldBounds": map[string]int{ - "MinX": -c.GetWorldRadius(), - "MinY": -c.GetWorldRadius(), - "MaxX": c.GetWorldRadius(), - "MaxY": c.GetWorldRadius(), - }, - } -} diff --git a/internal/drops/drops.go b/internal/drops/drops.go index a7ccf58..8dc13b3 100644 --- a/internal/drops/drops.go +++ b/internal/drops/drops.go @@ -11,6 +11,8 @@ import ( // Drop represents a drop item in the database type Drop struct { + database.BaseModel + ID int `db:"id" json:"id"` Name string `db:"name" json:"name"` Level int `db:"level" json:"level"` @@ -18,7 +20,31 @@ type Drop struct { Att string `db:"att" json:"att"` } -// New creates a new Drop with sensible defaults +func (d *Drop) GetTableName() string { + return "drops" +} + +func (d *Drop) GetID() int { + return d.ID +} + +func (d *Drop) SetID(id int) { + d.ID = id +} + +func (d *Drop) Set(field string, value any) error { + return database.Set(d, field, value) +} + +func (d *Drop) Save() error { + return database.Save(d) +} + +func (d *Drop) Delete() error { + return database.Delete(d) +} + +// Creates a new Drop with sensible defaults func New() *Drop { return &Drop{ Name: "", @@ -30,12 +56,12 @@ func New() *Drop { var dropScanner = scanner.New[Drop]() -// dropColumns returns the column list for drop queries +// Returns the column list for drop queries func dropColumns() string { return dropScanner.Columns() } -// scanDrop populates a Drop struct using the fast scanner +// Populates a Drop struct using the fast scanner func scanDrop(stmt *sqlite.Stmt) *Drop { drop := &Drop{} dropScanner.Scan(stmt, drop) @@ -47,7 +73,7 @@ const ( TypeConsumable = 1 ) -// Find retrieves a drop by ID +// Retrieves a drop by ID func Find(id int) (*Drop, error) { var drop *Drop @@ -69,7 +95,7 @@ func Find(id int) (*Drop, error) { return drop, nil } -// All retrieves all drops +// Retrieves all drops func All() ([]*Drop, error) { var drops []*Drop @@ -88,7 +114,7 @@ func All() ([]*Drop, error) { return drops, nil } -// ByLevel retrieves drops by minimum level requirement +// Retrieves drops by minimum level requirement func ByLevel(minLevel int) ([]*Drop, error) { var drops []*Drop @@ -107,7 +133,7 @@ func ByLevel(minLevel int) ([]*Drop, error) { return drops, nil } -// ByType retrieves drops by type +// Retrieves drops by type func ByType(dropType int) ([]*Drop, error) { var drops []*Drop @@ -126,62 +152,19 @@ func ByType(dropType int) ([]*Drop, error) { return drops, nil } -// Save updates an existing drop in the database -func (d *Drop) Save() error { - if d.ID == 0 { - return fmt.Errorf("cannot save drop without ID") - } - - query := `UPDATE drops SET name = ?, level = ?, type = ?, att = ? WHERE id = ?` - return database.Exec(query, d.Name, d.Level, d.Type, d.Att, d.ID) -} - -// Insert saves a new drop to the database and sets the ID +// Saves a new drop to the database and sets the ID func (d *Drop) Insert() error { - if d.ID != 0 { - return fmt.Errorf("drop already has ID %d, use Save() to update", d.ID) - } - - // Use a transaction to ensure we can get the ID - err := database.Transaction(func(tx *database.Tx) error { - query := `INSERT INTO drops (name, level, type, att) VALUES (?, ?, ?, ?)` - - if err := tx.Exec(query, d.Name, d.Level, d.Type, d.Att); err != nil { - return fmt.Errorf("failed to insert drop: %w", err) - } - - // Get the last insert ID - var id int - err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { - id = stmt.ColumnInt(0) - return nil - }) - if err != nil { - return fmt.Errorf("failed to get insert ID: %w", err) - } - - d.ID = id - return nil - }) - - return err + columns := `name, level, type, att` + values := []any{d.Name, d.Level, d.Type, d.Att} + return database.Insert(d, columns, values...) } -// Delete removes the drop from the database -func (d *Drop) Delete() error { - if d.ID == 0 { - return fmt.Errorf("cannot delete drop without ID") - } - - return database.Exec("DELETE FROM drops WHERE id = ?", d.ID) -} - -// IsConsumable returns true if the drop is a consumable item +// Returns true if the drop is a consumable item func (d *Drop) IsConsumable() bool { return d.Type == TypeConsumable } -// TypeName returns the string representation of the drop type +// Returns the string representation of the drop type func (d *Drop) TypeName() string { switch d.Type { case TypeConsumable: @@ -190,18 +173,3 @@ func (d *Drop) TypeName() string { return "Unknown" } } - -// ToMap converts the drop to a map for efficient template rendering -func (d *Drop) ToMap() map[string]any { - return map[string]any{ - "ID": d.ID, - "Name": d.Name, - "Level": d.Level, - "Type": d.Type, - "Att": d.Att, - - // Computed values - "IsConsumable": d.IsConsumable(), - "TypeName": d.TypeName(), - } -} diff --git a/internal/forum/forum.go b/internal/forum/forum.go index a669a8e..b5d9713 100644 --- a/internal/forum/forum.go +++ b/internal/forum/forum.go @@ -13,6 +13,8 @@ import ( // Forum represents a forum post or thread in the database type Forum struct { + database.BaseModel + ID int `db:"id" json:"id"` Posted int64 `db:"posted" json:"posted"` LastPost int64 `db:"last_post" json:"last_post"` @@ -23,7 +25,31 @@ type Forum struct { Content string `db:"content" json:"content"` } -// New creates a new Forum with sensible defaults +func (f *Forum) GetTableName() string { + return "forum" +} + +func (f *Forum) GetID() int { + return f.ID +} + +func (f *Forum) SetID(id int) { + f.ID = id +} + +func (f *Forum) Set(field string, value any) error { + return database.Set(f, field, value) +} + +func (f *Forum) Save() error { + return database.Save(f) +} + +func (f *Forum) Delete() error { + return database.Delete(f) +} + +// Creates a new Forum with sensible defaults func New() *Forum { now := time.Now().Unix() return &Forum{ @@ -39,19 +65,19 @@ func New() *Forum { var forumScanner = scanner.New[Forum]() -// forumColumns returns the column list for forum queries +// Returns the column list for forum queries func forumColumns() string { return forumScanner.Columns() } -// scanForum populates a Forum struct using the fast scanner +// Populates a Forum struct using the fast scanner func scanForum(stmt *sqlite.Stmt) *Forum { forum := &Forum{} forumScanner.Scan(stmt, forum) return forum } -// Find retrieves a forum post by ID +// Retrieves a forum post by ID func Find(id int) (*Forum, error) { var forum *Forum @@ -73,7 +99,7 @@ func Find(id int) (*Forum, error) { return forum, nil } -// All retrieves all forum posts ordered by last post time (most recent first) +// Retrieves all forum posts ordered by last post time (most recent first) func All() ([]*Forum, error) { var forums []*Forum @@ -92,7 +118,7 @@ func All() ([]*Forum, error) { return forums, nil } -// Threads retrieves all top-level forum threads (parent = 0) +// Retrieves all top-level forum threads (parent = 0) func Threads() ([]*Forum, error) { var forums []*Forum @@ -111,7 +137,7 @@ func Threads() ([]*Forum, error) { return forums, nil } -// ByParent retrieves all replies to a specific thread/post +// Retrieves all replies to a specific thread/post func ByParent(parentID int) ([]*Forum, error) { var forums []*Forum @@ -130,7 +156,7 @@ func ByParent(parentID int) ([]*Forum, error) { return forums, nil } -// ByAuthor retrieves forum posts by a specific author +// Retrieves forum posts by a specific author func ByAuthor(authorID int) ([]*Forum, error) { var forums []*Forum @@ -149,7 +175,7 @@ func ByAuthor(authorID int) ([]*Forum, error) { return forums, nil } -// Recent retrieves the most recent forum activity (limited by count) +// Retrieves the most recent forum activity (limited by count) func Recent(limit int) ([]*Forum, error) { var forums []*Forum @@ -168,7 +194,7 @@ func Recent(limit int) ([]*Forum, error) { return forums, nil } -// Search retrieves forum posts containing the search term in title or content +// Retrieves forum posts containing the search term in title or content func Search(term string) ([]*Forum, error) { var forums []*Forum @@ -188,7 +214,7 @@ func Search(term string) ([]*Forum, error) { return forums, nil } -// Since retrieves forum posts with activity since a specific timestamp +// Retrieves forum posts with activity since a specific timestamp func Since(since int64) ([]*Forum, error) { var forums []*Forum @@ -207,112 +233,69 @@ func Since(since int64) ([]*Forum, error) { return forums, nil } -// Save updates an existing forum post in the database -func (f *Forum) Save() error { - if f.ID == 0 { - return fmt.Errorf("cannot save forum post without ID") - } - - query := `UPDATE forum SET posted = ?, last_post = ?, author = ?, parent = ?, replies = ?, title = ?, content = ? WHERE id = ?` - return database.Exec(query, f.Posted, f.LastPost, f.Author, f.Parent, f.Replies, f.Title, f.Content, f.ID) -} - -// Insert saves a new forum post to the database and sets the ID +// Saves a new forum post to the database and sets the ID func (f *Forum) Insert() error { - if f.ID != 0 { - return fmt.Errorf("forum post already has ID %d, use Save() to update", f.ID) - } - - // Use a transaction to ensure we can get the ID - err := database.Transaction(func(tx *database.Tx) error { - query := `INSERT INTO forum (posted, last_post, author, parent, replies, title, content) VALUES (?, ?, ?, ?, ?, ?, ?)` - - if err := tx.Exec(query, f.Posted, f.LastPost, f.Author, f.Parent, f.Replies, f.Title, f.Content); err != nil { - return fmt.Errorf("failed to insert forum post: %w", err) - } - - // Get the last insert ID - var id int - err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { - id = stmt.ColumnInt(0) - return nil - }) - if err != nil { - return fmt.Errorf("failed to get insert ID: %w", err) - } - - f.ID = id - return nil - }) - - return err + columns := `posted, last_post, author, parent, replies, title, content` + values := []any{f.Posted, f.LastPost, f.Author, f.Parent, f.Replies, f.Title, f.Content} + return database.Insert(f, columns, values...) } -// Delete removes the forum post from the database -func (f *Forum) Delete() error { - if f.ID == 0 { - return fmt.Errorf("cannot delete forum post without ID") - } - - return database.Exec("DELETE FROM forum WHERE id = ?", f.ID) -} - -// PostedTime returns the posted timestamp as a time.Time +// Returns the posted timestamp as a time.Time func (f *Forum) PostedTime() time.Time { return time.Unix(f.Posted, 0) } -// LastPostTime returns the last post timestamp as a time.Time +// Returns the last post timestamp as a time.Time func (f *Forum) LastPostTime() time.Time { return time.Unix(f.LastPost, 0) } -// SetPostedTime sets the posted timestamp from a time.Time +// Sets the posted timestamp from a time.Time func (f *Forum) SetPostedTime(t time.Time) { - f.Posted = t.Unix() + f.Set("Posted", t.Unix()) } -// SetLastPostTime sets the last post timestamp from a time.Time +// Sets the last post timestamp from a time.Time func (f *Forum) SetLastPostTime(t time.Time) { - f.LastPost = t.Unix() + f.Set("LastPost", t.Unix()) } -// IsThread returns true if this is a top-level thread (parent = 0) +// Returns true if this is a top-level thread (parent = 0) func (f *Forum) IsThread() bool { return f.Parent == 0 } -// IsReply returns true if this is a reply to another post (parent > 0) +// Returns true if this is a reply to another post (parent > 0) func (f *Forum) IsReply() bool { return f.Parent > 0 } -// HasReplies returns true if this post has replies +// Returns true if this post has replies func (f *Forum) HasReplies() bool { return f.Replies > 0 } -// IsRecentActivity returns true if there has been activity within the last 24 hours +// Returns true if there has been activity within the last 24 hours func (f *Forum) IsRecentActivity() bool { return time.Since(f.LastPostTime()) < 24*time.Hour } -// ActivityAge returns how long ago the last activity occurred +// Returns how long ago the last activity occurred func (f *Forum) ActivityAge() time.Duration { return time.Since(f.LastPostTime()) } -// PostAge returns how long ago the post was originally made +// Returns how long ago the post was originally made func (f *Forum) PostAge() time.Duration { return time.Since(f.PostedTime()) } -// IsAuthor returns true if the given user ID is the author of this post +// Returns true if the given user ID is the author of this post func (f *Forum) IsAuthor(userID int) bool { return f.Author == userID } -// Preview returns a truncated version of the content for previews +// Returns a truncated version of the content for previews func (f *Forum) Preview(maxLength int) string { if len(f.Content) <= maxLength { return f.Content @@ -325,7 +308,7 @@ func (f *Forum) Preview(maxLength int) string { return f.Content[:maxLength-3] + "..." } -// WordCount returns the number of words in the content +// Returns the number of words in the content func (f *Forum) WordCount() int { if f.Content == "" { return 0 @@ -353,70 +336,44 @@ func (f *Forum) WordCount() int { return words } -// Length returns the character length of the content +// Returns the character length of the content func (f *Forum) Length() int { return len(f.Content) } -// Contains returns true if the title or content contains the given term (case-insensitive) +// Returns true if the title or content contains the given term (case-insensitive) func (f *Forum) Contains(term string) bool { lowerTerm := strings.ToLower(term) return strings.Contains(strings.ToLower(f.Title), lowerTerm) || strings.Contains(strings.ToLower(f.Content), lowerTerm) } -// UpdateLastPost updates the last_post timestamp to current time +// Updates the last_post timestamp to current time func (f *Forum) UpdateLastPost() { - f.LastPost = time.Now().Unix() + f.Set("LastPost", time.Now().Unix()) } -// IncrementReplies increments the reply count +// Increments the reply count func (f *Forum) IncrementReplies() { - f.Replies++ + f.Set("Replies", f.Replies+1) } -// DecrementReplies decrements the reply count (minimum 0) +// Decrements the reply count (minimum 0) func (f *Forum) DecrementReplies() { if f.Replies > 0 { - f.Replies-- + f.Set("Replies", f.Replies-1) } } -// GetReplies retrieves all direct replies to this post +// Retrieves all direct replies to this post func (f *Forum) GetReplies() ([]*Forum, error) { return ByParent(f.ID) } -// GetThread retrieves the parent thread (if this is a reply) or returns self (if this is a thread) +// Retrieves the parent thread (if this is a reply) or returns self (if this is a thread) func (f *Forum) GetThread() (*Forum, error) { if f.IsThread() { return f, nil } return Find(f.Parent) } - -// ToMap converts the forum post to a map for efficient template rendering -func (f *Forum) ToMap() map[string]any { - return map[string]any{ - "ID": f.ID, - "Posted": f.Posted, - "LastPost": f.LastPost, - "Author": f.Author, - "Parent": f.Parent, - "Replies": f.Replies, - "Title": f.Title, - "Content": f.Content, - - // Computed values - "PostedTime": f.PostedTime(), - "LastPostTime": f.LastPostTime(), - "IsThread": f.IsThread(), - "IsReply": f.IsReply(), - "HasReplies": f.HasReplies(), - "IsRecentActivity": f.IsRecentActivity(), - "ActivityAge": f.ActivityAge(), - "PostAge": f.PostAge(), - "WordCount": f.WordCount(), - "Length": f.Length(), - } -} diff --git a/internal/helpers/ordered_map.go b/internal/helpers/ordered_map.go index 10b1b42..13f00c0 100644 --- a/internal/helpers/ordered_map.go +++ b/internal/helpers/ordered_map.go @@ -27,13 +27,10 @@ func (om *OrderedMap[K, V]) Range(fn func(K, V) bool) { } } -func (om *OrderedMap[K, V]) ToSlice() []map[string]any { - result := make([]map[string]any, 0, len(om.keys)) +func (om *OrderedMap[K, V]) ToSlice() []V { + result := make([]V, 0, len(om.keys)) for _, key := range om.keys { - result = append(result, map[string]any{ - "id": key, - "name": om.data[key], - }) + result = append(result, om.data[key]) } return result } diff --git a/internal/items/items.go b/internal/items/items.go index c88da49..bd34fda 100644 --- a/internal/items/items.go +++ b/internal/items/items.go @@ -11,6 +11,8 @@ import ( // Item represents an item in the database type Item struct { + database.BaseModel + ID int `db:"id" json:"id"` Type int `db:"type" json:"type"` Name string `db:"name" json:"name"` @@ -19,7 +21,31 @@ type Item struct { Special string `db:"special" json:"special"` } -// New creates a new Item with sensible defaults +func (i *Item) GetTableName() string { + return "items" +} + +func (i *Item) GetID() int { + return i.ID +} + +func (i *Item) SetID(id int) { + i.ID = id +} + +func (i *Item) Set(field string, value any) error { + return database.Set(i, field, value) +} + +func (i *Item) Save() error { + return database.Save(i) +} + +func (i *Item) Delete() error { + return database.Delete(i) +} + +// Creates a new Item with sensible defaults func New() *Item { return &Item{ Type: TypeWeapon, // Default to weapon @@ -32,12 +58,12 @@ func New() *Item { var itemScanner = scanner.New[Item]() -// itemColumns returns the column list for item queries +// Returns the column list for item queries func itemColumns() string { return itemScanner.Columns() } -// scanItem populates an Item struct using the fast scanner +// Populates an Item struct using the fast scanner func scanItem(stmt *sqlite.Stmt) *Item { item := &Item{} itemScanner.Scan(stmt, item) @@ -51,7 +77,7 @@ const ( TypeShield = 3 ) -// Find retrieves an item by ID +// Retrieves an item by ID func Find(id int) (*Item, error) { var item *Item @@ -73,7 +99,7 @@ func Find(id int) (*Item, error) { return item, nil } -// All retrieves all items +// Retrieves all items func All() ([]*Item, error) { var items []*Item @@ -92,7 +118,7 @@ func All() ([]*Item, error) { return items, nil } -// ByType retrieves items by type +// Retrieves items by type func ByType(itemType int) ([]*Item, error) { var items []*Item @@ -111,73 +137,29 @@ func ByType(itemType int) ([]*Item, error) { return items, nil } -// Save updates an existing item in the database -func (i *Item) Save() error { - if i.ID == 0 { - return fmt.Errorf("cannot save item without ID") - } - - query := `UPDATE items SET type = ?, name = ?, value = ?, att = ?, special = ? WHERE id = ?` - return database.Exec(query, i.Type, i.Name, i.Value, i.Att, i.Special, i.ID) -} - -// Insert saves a new item to the database and sets the ID +// Saves a new item to the database and sets the ID func (i *Item) Insert() error { - if i.ID != 0 { - return fmt.Errorf("item already has ID %d, use Save() to update", i.ID) - } - - // Use a transaction to ensure we can get the ID - err := database.Transaction(func(tx *database.Tx) error { - query := `INSERT INTO items (type, name, value, att, special) VALUES (?, ?, ?, ?, ?)` - - if err := tx.Exec(query, i.Type, i.Name, i.Value, i.Att, i.Special); err != nil { - return fmt.Errorf("failed to insert item: %w", err) - } - - // Get the last insert ID - var id int - err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { - id = stmt.ColumnInt(0) - return nil - }) - if err != nil { - return fmt.Errorf("failed to get insert ID: %w", err) - } - - i.ID = id - return nil - }) - - return err + columns := `type, name, value, att, special` + values := []any{i.Type, i.Name, i.Value, i.Att, i.Special} + return database.Insert(i, columns, values...) } -// Delete removes the item from the database -func (i *Item) Delete() error { - if i.ID == 0 { - return fmt.Errorf("cannot delete item without ID") - } - - query := "DELETE FROM items WHERE id = ?" - return database.Exec(query, i.ID) -} - -// IsWeapon returns true if the item is a weapon +// Returns true if the item is a weapon func (i *Item) IsWeapon() bool { return i.Type == TypeWeapon } -// IsArmor returns true if the item is armor +// Returns true if the item is armor func (i *Item) IsArmor() bool { return i.Type == TypeArmor } -// IsShield returns true if the item is a shield +// Returns true if the item is a shield func (i *Item) IsShield() bool { return i.Type == TypeShield } -// TypeName returns the string representation of the item type +// Returns the string representation of the item type func (i *Item) TypeName() string { switch i.Type { case TypeWeapon: @@ -191,32 +173,12 @@ func (i *Item) TypeName() string { } } -// HasSpecial returns true if the item has special properties +// Returns true if the item has special properties func (i *Item) HasSpecial() bool { return i.Special != "" } -// IsEquippable returns true if the item can be equipped +// Returns true if the item can be equipped func (i *Item) IsEquippable() bool { return i.Type == TypeWeapon || i.Type == TypeArmor || i.Type == TypeShield } - -// ToMap converts the item to a map for efficient template rendering -func (i *Item) ToMap() map[string]any { - return map[string]any{ - "ID": i.ID, - "Type": i.Type, - "Name": i.Name, - "Value": i.Value, - "Att": i.Att, - "Special": i.Special, - - // Computed values - "IsWeapon": i.IsWeapon(), - "IsArmor": i.IsArmor(), - "IsShield": i.IsShield(), - "TypeName": i.TypeName(), - "HasSpecial": i.HasSpecial(), - "IsEquippable": i.IsEquippable(), - } -} diff --git a/internal/monsters/monsters.go b/internal/monsters/monsters.go index b5f81a6..c4a5aa5 100644 --- a/internal/monsters/monsters.go +++ b/internal/monsters/monsters.go @@ -11,6 +11,8 @@ import ( // Monster represents a monster in the database type Monster struct { + database.BaseModel + ID int `db:"id" json:"id"` Name string `db:"name" json:"name"` MaxHP int `db:"max_hp" json:"max_hp"` @@ -22,28 +24,52 @@ type Monster struct { Immune int `db:"immune" json:"immune"` } -// New creates a new Monster with sensible defaults +func (m *Monster) GetTableName() string { + return "monsters" +} + +func (m *Monster) GetID() int { + return m.ID +} + +func (m *Monster) SetID(id int) { + m.ID = id +} + +func (m *Monster) Set(field string, value any) error { + return database.Set(m, field, value) +} + +func (m *Monster) Save() error { + return database.Save(m) +} + +func (m *Monster) Delete() error { + return database.Delete(m) +} + +// Creates a new Monster with sensible defaults func New() *Monster { return &Monster{ Name: "", - MaxHP: 10, // Default HP - MaxDmg: 5, // Default damage - Armor: 0, // Default armor - Level: 1, // Default level - MaxExp: 10, // Default exp reward - MaxGold: 5, // Default gold reward + MaxHP: 10, // Default HP + MaxDmg: 5, // Default damage + Armor: 0, // Default armor + Level: 1, // Default level + MaxExp: 10, // Default exp reward + MaxGold: 5, // Default gold reward Immune: ImmuneNone, // No immunity by default } } var monsterScanner = scanner.New[Monster]() -// monsterColumns returns the column list for monster queries +// Returns the column list for monster queries func monsterColumns() string { return monsterScanner.Columns() } -// scanMonster populates a Monster struct using the fast scanner +// Populates a Monster struct using the fast scanner func scanMonster(stmt *sqlite.Stmt) *Monster { monster := &Monster{} monsterScanner.Scan(stmt, monster) @@ -57,7 +83,7 @@ const ( ImmuneSleep = 2 // Immune to Sleep spells ) -// Find retrieves a monster by ID +// Retrieves a monster by ID func Find(id int) (*Monster, error) { var monster *Monster @@ -79,7 +105,7 @@ func Find(id int) (*Monster, error) { return monster, nil } -// All retrieves all monsters +// Retrieves all monsters func All() ([]*Monster, error) { var monsters []*Monster @@ -98,7 +124,7 @@ func All() ([]*Monster, error) { return monsters, nil } -// ByLevel retrieves monsters by level +// Retrieves monsters by level func ByLevel(level int) ([]*Monster, error) { var monsters []*Monster @@ -117,7 +143,7 @@ func ByLevel(level int) ([]*Monster, error) { return monsters, nil } -// ByLevelRange retrieves monsters within a level range (inclusive) +// Retrieves monsters within a level range (inclusive) func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) { var monsters []*Monster @@ -136,7 +162,7 @@ func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) { return monsters, nil } -// ByImmunity retrieves monsters by immunity type +// Retrieves monsters by immunity type func ByImmunity(immunityType int) ([]*Monster, error) { var monsters []*Monster @@ -155,73 +181,29 @@ func ByImmunity(immunityType int) ([]*Monster, error) { return monsters, nil } -// Save updates an existing monster in the database -func (m *Monster) Save() error { - if m.ID == 0 { - return fmt.Errorf("cannot save monster without ID") - } - - query := `UPDATE monsters SET name = ?, max_hp = ?, max_dmg = ?, armor = ?, level = ?, max_exp = ?, max_gold = ?, immune = ? WHERE id = ?` - return database.Exec(query, m.Name, m.MaxHP, m.MaxDmg, m.Armor, m.Level, m.MaxExp, m.MaxGold, m.Immune, m.ID) -} - -// Insert saves a new monster to the database and sets the ID +// Saves a new monster to the database and sets the ID func (m *Monster) Insert() error { - if m.ID != 0 { - return fmt.Errorf("monster already has ID %d, use Save() to update", m.ID) - } - - // Use a transaction to ensure we can get the ID - err := database.Transaction(func(tx *database.Tx) error { - query := `INSERT INTO monsters (name, max_hp, max_dmg, armor, level, max_exp, max_gold, immune) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` - - if err := tx.Exec(query, m.Name, m.MaxHP, m.MaxDmg, m.Armor, m.Level, m.MaxExp, m.MaxGold, m.Immune); err != nil { - return fmt.Errorf("failed to insert monster: %w", err) - } - - // Get the last insert ID - var id int - err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { - id = stmt.ColumnInt(0) - return nil - }) - if err != nil { - return fmt.Errorf("failed to get insert ID: %w", err) - } - - m.ID = id - return nil - }) - - return err + columns := `name, max_hp, max_dmg, armor, level, max_exp, max_gold, immune` + values := []any{m.Name, m.MaxHP, m.MaxDmg, m.Armor, m.Level, m.MaxExp, m.MaxGold, m.Immune} + return database.Insert(m, columns, values...) } -// Delete removes the monster from the database -func (m *Monster) Delete() error { - if m.ID == 0 { - return fmt.Errorf("cannot delete monster without ID") - } - - query := "DELETE FROM monsters WHERE id = ?" - return database.Exec(query, m.ID) -} - -// IsHurtImmune returns true if the monster is immune to Hurt spells +// Returns true if the monster is immune to Hurt spells func (m *Monster) IsHurtImmune() bool { return m.Immune == ImmuneHurt } -// IsSleepImmune returns true if the monster is immune to Sleep spells +// Returns true if the monster is immune to Sleep spells func (m *Monster) IsSleepImmune() bool { return m.Immune == ImmuneSleep } -// HasImmunity returns true if the monster has any immunity +// Returns true if the monster has any immunity func (m *Monster) HasImmunity() bool { return m.Immune != ImmuneNone } -// ImmunityName returns the string representation of the monster's immunity +// Returns the string representation of the monster's immunity func (m *Monster) ImmunityName() string { switch m.Immune { case ImmuneNone: @@ -235,7 +217,7 @@ func (m *Monster) ImmunityName() string { } } -// DifficultyRating calculates a simple difficulty rating based on stats +// Calculates a simple difficulty rating based on stats func (m *Monster) DifficultyRating() float64 { // Simple formula: (HP + Damage + Armor) / Level // Higher values indicate tougher monsters relative to their level @@ -245,7 +227,7 @@ func (m *Monster) DifficultyRating() float64 { return float64(m.MaxHP+m.MaxDmg+m.Armor) / float64(m.Level) } -// ExpPerHP returns the experience reward per hit point (efficiency metric) +// Returns the experience reward per hit point (efficiency metric) func (m *Monster) ExpPerHP() float64 { if m.MaxHP == 0 { return 0 @@ -253,34 +235,10 @@ func (m *Monster) ExpPerHP() float64 { return float64(m.MaxExp) / float64(m.MaxHP) } -// GoldPerHP returns the gold reward per hit point (efficiency metric) +// Returns the gold reward per hit point (efficiency metric) func (m *Monster) GoldPerHP() float64 { if m.MaxHP == 0 { return 0 } return float64(m.MaxGold) / float64(m.MaxHP) } - -// ToMap converts the monster to a map for efficient template rendering -func (m *Monster) ToMap() map[string]any { - return map[string]any{ - "ID": m.ID, - "Name": m.Name, - "MaxHP": m.MaxHP, - "MaxDmg": m.MaxDmg, - "Armor": m.Armor, - "Level": m.Level, - "MaxExp": m.MaxExp, - "MaxGold": m.MaxGold, - "Immune": m.Immune, - - // Computed values - "IsHurtImmune": m.IsHurtImmune(), - "IsSleepImmune": m.IsSleepImmune(), - "HasImmunity": m.HasImmunity(), - "ImmunityName": m.ImmunityName(), - "DifficultyRating": m.DifficultyRating(), - "ExpPerHP": m.ExpPerHP(), - "GoldPerHP": m.GoldPerHP(), - } -} diff --git a/internal/news/news.go b/internal/news/news.go index a06e9e8..70cc8c5 100644 --- a/internal/news/news.go +++ b/internal/news/news.go @@ -12,13 +12,39 @@ import ( // News represents a news post in the database type News struct { + database.BaseModel + ID int `db:"id" json:"id"` Author int `db:"author" json:"author"` Posted int64 `db:"posted" json:"posted"` Content string `db:"content" json:"content"` } -// New creates a new News with sensible defaults +func (n *News) GetTableName() string { + return "news" +} + +func (n *News) GetID() int { + return n.ID +} + +func (n *News) SetID(id int) { + n.ID = id +} + +func (n *News) Set(field string, value any) error { + return database.Set(n, field, value) +} + +func (n *News) Save() error { + return database.Save(n) +} + +func (n *News) Delete() error { + return database.Delete(n) +} + +// Creates a new News with sensible defaults func New() *News { return &News{ Author: 0, // No author by default @@ -29,19 +55,19 @@ func New() *News { var newsScanner = scanner.New[News]() -// newsColumns returns the column list for news queries +// Returns the column list for news queries func newsColumns() string { return newsScanner.Columns() } -// scanNews populates a News struct using the fast scanner +// Populates a News struct using the fast scanner func scanNews(stmt *sqlite.Stmt) *News { news := &News{} newsScanner.Scan(stmt, news) return news } -// Find retrieves a news post by ID +// Retrieves a news post by ID func Find(id int) (*News, error) { var news *News @@ -63,7 +89,7 @@ func Find(id int) (*News, error) { return news, nil } -// All retrieves all news posts ordered by posted date (newest first) +// Retrieves all news posts ordered by posted date (newest first) func All() ([]*News, error) { var newsPosts []*News @@ -82,7 +108,7 @@ func All() ([]*News, error) { return newsPosts, nil } -// ByAuthor retrieves news posts by a specific author +// Retrieves news posts by a specific author func ByAuthor(authorID int) ([]*News, error) { var newsPosts []*News @@ -101,7 +127,7 @@ func ByAuthor(authorID int) ([]*News, error) { return newsPosts, nil } -// Recent retrieves the most recent news posts (limited by count) +// Retrieves the most recent news posts (limited by count) func Recent(limit int) ([]*News, error) { var newsPosts []*News @@ -120,7 +146,7 @@ func Recent(limit int) ([]*News, error) { return newsPosts, nil } -// Since retrieves news posts since a specific timestamp +// Retrieves news posts since a specific timestamp func Since(since int64) ([]*News, error) { var newsPosts []*News @@ -139,7 +165,7 @@ func Since(since int64) ([]*News, error) { return newsPosts, nil } -// Between retrieves news posts between two timestamps (inclusive) +// Retrieves news posts between two timestamps (inclusive) func Between(start, end int64) ([]*News, error) { var newsPosts []*News @@ -158,88 +184,44 @@ func Between(start, end int64) ([]*News, error) { return newsPosts, nil } -// Save updates an existing news post in the database -func (n *News) Save() error { - if n.ID == 0 { - return fmt.Errorf("cannot save news without ID") - } - - query := `UPDATE news SET author = ?, posted = ?, content = ? WHERE id = ?` - return database.Exec(query, n.Author, n.Posted, n.Content, n.ID) -} - -// Insert saves a new news post to the database and sets the ID +// Saves a new news post to the database and sets the ID func (n *News) Insert() error { - if n.ID != 0 { - return fmt.Errorf("news already has ID %d, use Save() to update", n.ID) - } - - // Use a transaction to ensure we can get the ID - err := database.Transaction(func(tx *database.Tx) error { - query := `INSERT INTO news (author, posted, content) VALUES (?, ?, ?)` - - if err := tx.Exec(query, n.Author, n.Posted, n.Content); err != nil { - return fmt.Errorf("failed to insert news: %w", err) - } - - // Get the last insert ID - var id int - err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { - id = stmt.ColumnInt(0) - return nil - }) - if err != nil { - return fmt.Errorf("failed to get insert ID: %w", err) - } - - n.ID = id - return nil - }) - - return err + columns := `author, posted, content` + values := []any{n.Author, n.Posted, n.Content} + return database.Insert(n, columns, values...) } -// Delete removes the news post from the database -func (n *News) Delete() error { - if n.ID == 0 { - return fmt.Errorf("cannot delete news without ID") - } - - query := "DELETE FROM news WHERE id = ?" - return database.Exec(query, n.ID) -} - -// PostedTime returns the posted timestamp as a time.Time +// Returns the posted timestamp as a time.Time func (n *News) PostedTime() time.Time { return time.Unix(n.Posted, 0) } -// SetPostedTime sets the posted timestamp from a time.Time +// Sets the posted timestamp from a time.Time func (n *News) SetPostedTime(t time.Time) { - n.Posted = t.Unix() + n.Set("Posted", t.Unix()) } -// IsRecent returns true if the news post was made within the last 24 hours +// Returns true if the news post was made within the last 24 hours func (n *News) IsRecent() bool { return time.Since(n.PostedTime()) < 24*time.Hour } -// Age returns how long ago the news post was made +// Returns how long ago the news post was made func (n *News) Age() time.Duration { return time.Since(n.PostedTime()) } -// ReadableTime converts a time.Time to a human-readable date string +// Converts a time.Time to a human-readable date string func (n *News) ReadableTime() string { return n.PostedTime().Format("Jan 2, 2006 3:04 PM") } -// IsAuthor returns true if the given user ID is the author of this news post +// Returns true if the given user ID is the author of this news post func (n *News) IsAuthor(userID int) bool { return n.Author == userID } -// Preview returns a truncated version of the content for previews +// Returns a truncated version of the content for previews func (n *News) Preview(maxLength int) string { if len(n.Content) <= maxLength { return n.Content @@ -252,7 +234,7 @@ func (n *News) Preview(maxLength int) string { return n.Content[:maxLength-3] + "..." } -// WordCount returns the number of words in the content +// Returns the number of words in the content func (n *News) WordCount() int { if n.Content == "" { return 0 @@ -280,22 +262,22 @@ func (n *News) WordCount() int { return words } -// Length returns the character length of the content +// Returns the character length of the content func (n *News) Length() int { return len(n.Content) } -// Contains returns true if the content contains the given term (case-insensitive) +// Returns true if the content contains the given term (case-insensitive) func (n *News) Contains(term string) bool { return strings.Contains(strings.ToLower(n.Content), strings.ToLower(term)) } -// IsEmpty returns true if the content is empty or whitespace-only +// Returns true if the content is empty or whitespace-only func (n *News) IsEmpty() bool { return strings.TrimSpace(n.Content) == "" } -// Search retrieves news posts containing the search term in content +// Retrieves news posts containing the search term in content func Search(term string) ([]*News, error) { var newsPosts []*News @@ -314,22 +296,3 @@ func Search(term string) ([]*News, error) { return newsPosts, nil } - -// ToMap converts the news to a map for efficient template rendering -func (n *News) ToMap() map[string]any { - return map[string]any{ - "ID": n.ID, - "Author": n.Author, - "Posted": n.Posted, - "Content": n.Content, - - // Computed values - "PostedTime": n.PostedTime(), - "IsRecent": n.IsRecent(), - "Age": n.Age(), - "ReadableTime": n.ReadableTime(), - "WordCount": n.WordCount(), - "Length": n.Length(), - "IsEmpty": n.IsEmpty(), - } -} diff --git a/internal/routes/town.go b/internal/routes/town.go index aa4e3a5..3467a45 100644 --- a/internal/routes/town.go +++ b/internal/routes/town.go @@ -2,6 +2,8 @@ package routes import ( "dk/internal/auth" + "dk/internal/helpers" + "dk/internal/items" "dk/internal/middleware" "dk/internal/router" "dk/internal/template/components" @@ -17,6 +19,7 @@ func RegisterTownRoutes(r *router.Router) { group.Get("/", showTown) group.Get("/inn", showInn) + group.Get("/shop", showShop) group.WithMiddleware(middleware.CSRF(auth.Manager)).Post("/inn", rest) } @@ -58,3 +61,23 @@ func rest(ctx router.Ctx, _ []string) { "rested": true, }) } + +func showShop(ctx router.Ctx, _ []string) { + town := ctx.UserValue("town").(*towns.Town) + + var itemlist []*items.Item + if town.ShopList != "" { + itemMap := helpers.NewOrderedMap[int, *items.Item]() + for _, id := range town.GetShopItems() { + if item, err := items.Find(id); err == nil { + itemMap.Set(id, item) + } + } + itemlist = itemMap.ToSlice() + } + + components.RenderPage(ctx, town.Name+" Shop", "town/shop.html", map[string]any{ + "town": town, + "itemlist": itemlist, + }) +} diff --git a/internal/spells/spells.go b/internal/spells/spells.go index 1b063a3..28f0b12 100644 --- a/internal/spells/spells.go +++ b/internal/spells/spells.go @@ -11,6 +11,8 @@ import ( // Spell represents a spell in the database type Spell struct { + database.BaseModel + ID int `db:"id" json:"id"` Name string `db:"name" json:"name"` MP int `db:"mp" json:"mp"` @@ -18,7 +20,31 @@ type Spell struct { Type int `db:"type" json:"type"` } -// New creates a new Spell with sensible defaults +func (s *Spell) GetTableName() string { + return "spells" +} + +func (s *Spell) GetID() int { + return s.ID +} + +func (s *Spell) SetID(id int) { + s.ID = id +} + +func (s *Spell) Set(field string, value any) error { + return database.Set(s, field, value) +} + +func (s *Spell) Save() error { + return database.Save(s) +} + +func (s *Spell) Delete() error { + return database.Delete(s) +} + +// Creates a new Spell with sensible defaults func New() *Spell { return &Spell{ Name: "", @@ -30,12 +56,12 @@ func New() *Spell { var spellScanner = scanner.New[Spell]() -// spellColumns returns the column list for spell queries +// Returns the column list for spell queries func spellColumns() string { return spellScanner.Columns() } -// scanSpell populates a Spell struct using the fast scanner +// Populates a Spell struct using the fast scanner func scanSpell(stmt *sqlite.Stmt) *Spell { spell := &Spell{} spellScanner.Scan(stmt, spell) @@ -51,7 +77,7 @@ const ( TypeDefenseBoost = 5 ) -// Find retrieves a spell by ID +// Retrieves a spell by ID func Find(id int) (*Spell, error) { var spell *Spell @@ -73,7 +99,7 @@ func Find(id int) (*Spell, error) { return spell, nil } -// All retrieves all spells +// Retrieves all spells func All() ([]*Spell, error) { var spells []*Spell @@ -92,7 +118,7 @@ func All() ([]*Spell, error) { return spells, nil } -// ByType retrieves spells by type +// Retrieves spells by type func ByType(spellType int) ([]*Spell, error) { var spells []*Spell @@ -111,7 +137,7 @@ func ByType(spellType int) ([]*Spell, error) { return spells, nil } -// ByMaxMP retrieves spells that cost at most the specified MP +// Retrieves spells that cost at most the specified MP func ByMaxMP(maxMP int) ([]*Spell, error) { var spells []*Spell @@ -130,7 +156,7 @@ func ByMaxMP(maxMP int) ([]*Spell, error) { return spells, nil } -// ByTypeAndMaxMP retrieves spells of a specific type that cost at most the specified MP +// Retrieves spells of a specific type that cost at most the specified MP func ByTypeAndMaxMP(spellType, maxMP int) ([]*Spell, error) { var spells []*Spell @@ -149,7 +175,7 @@ func ByTypeAndMaxMP(spellType, maxMP int) ([]*Spell, error) { return spells, nil } -// ByName retrieves a spell by name (case-insensitive) +// Retrieves a spell by name (case-insensitive) func ByName(name string) (*Spell, error) { var spell *Spell @@ -171,83 +197,39 @@ func ByName(name string) (*Spell, error) { return spell, nil } -// Save updates an existing spell in the database -func (s *Spell) Save() error { - if s.ID == 0 { - return fmt.Errorf("cannot save spell without ID") - } - - query := `UPDATE spells SET name = ?, mp = ?, attribute = ?, type = ? WHERE id = ?` - return database.Exec(query, s.Name, s.MP, s.Attribute, s.Type, s.ID) -} - -// Insert saves a new spell to the database and sets the ID +// Saves a new spell to the database and sets the ID func (s *Spell) Insert() error { - if s.ID != 0 { - return fmt.Errorf("spell already has ID %d, use Save() to update", s.ID) - } - - // Use a transaction to ensure we can get the ID - err := database.Transaction(func(tx *database.Tx) error { - query := `INSERT INTO spells (name, mp, attribute, type) VALUES (?, ?, ?, ?)` - - if err := tx.Exec(query, s.Name, s.MP, s.Attribute, s.Type); err != nil { - return fmt.Errorf("failed to insert spell: %w", err) - } - - // Get the last insert ID - var id int - err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { - id = stmt.ColumnInt(0) - return nil - }) - if err != nil { - return fmt.Errorf("failed to get insert ID: %w", err) - } - - s.ID = id - return nil - }) - - return err + columns := `name, mp, attribute, type` + values := []any{s.Name, s.MP, s.Attribute, s.Type} + return database.Insert(s, columns, values...) } -// Delete removes the spell from the database -func (s *Spell) Delete() error { - if s.ID == 0 { - return fmt.Errorf("cannot delete spell without ID") - } - - query := "DELETE FROM spells WHERE id = ?" - return database.Exec(query, s.ID) -} - -// IsHealing returns true if the spell is a healing spell +// Returns true if the spell is a healing spell func (s *Spell) IsHealing() bool { return s.Type == TypeHealing } -// IsHurt returns true if the spell is a hurt spell +// Returns true if the spell is a hurt spell func (s *Spell) IsHurt() bool { return s.Type == TypeHurt } -// IsSleep returns true if the spell is a sleep spell +// Returns true if the spell is a sleep spell func (s *Spell) IsSleep() bool { return s.Type == TypeSleep } -// IsAttackBoost returns true if the spell boosts attack +// Returns true if the spell boosts attack func (s *Spell) IsAttackBoost() bool { return s.Type == TypeAttackBoost } -// IsDefenseBoost returns true if the spell boosts defense +// Returns true if the spell boosts defense func (s *Spell) IsDefenseBoost() bool { return s.Type == TypeDefenseBoost } -// TypeName returns the string representation of the spell type +// Returns the string representation of the spell type func (s *Spell) TypeName() string { switch s.Type { case TypeHealing: @@ -265,12 +247,12 @@ func (s *Spell) TypeName() string { } } -// CanCast returns true if the spell can be cast with the given MP +// Returns true if the spell can be cast with the given MP func (s *Spell) CanCast(availableMP int) bool { return availableMP >= s.MP } -// Efficiency returns the attribute per MP ratio (higher is more efficient) +// Returns the attribute per MP ratio (higher is more efficient) func (s *Spell) Efficiency() float64 { if s.MP == 0 { return 0 @@ -278,34 +260,12 @@ func (s *Spell) Efficiency() float64 { return float64(s.Attribute) / float64(s.MP) } -// IsOffensive returns true if the spell is used for attacking +// Returns true if the spell is used for attacking func (s *Spell) IsOffensive() bool { return s.Type == TypeHurt || s.Type == TypeSleep } -// IsSupport returns true if the spell is used for support/buffs +// Returns true if the spell is used for support/buffs func (s *Spell) IsSupport() bool { return s.Type == TypeHealing || s.Type == TypeAttackBoost || s.Type == TypeDefenseBoost } - -// ToMap converts the spell to a map for efficient template rendering -func (s *Spell) ToMap() map[string]any { - return map[string]any{ - "ID": s.ID, - "Name": s.Name, - "MP": s.MP, - "Attribute": s.Attribute, - "Type": s.Type, - - // Computed values - "IsHealing": s.IsHealing(), - "IsHurt": s.IsHurt(), - "IsSleep": s.IsSleep(), - "IsAttackBoost": s.IsAttackBoost(), - "IsDefenseBoost": s.IsDefenseBoost(), - "TypeName": s.TypeName(), - "Efficiency": s.Efficiency(), - "IsOffensive": s.IsOffensive(), - "IsSupport": s.IsSupport(), - } -} diff --git a/internal/towns/towns.go b/internal/towns/towns.go index 43ae8af..a693686 100644 --- a/internal/towns/towns.go +++ b/internal/towns/towns.go @@ -3,9 +3,10 @@ package towns import ( "fmt" "math" - "strings" + "slices" "dk/internal/database" + "dk/internal/helpers" "dk/internal/helpers/scanner" "zombiezen.com/go/sqlite" @@ -13,6 +14,8 @@ import ( // Town represents a town in the database type Town struct { + database.BaseModel + ID int `db:"id" json:"id"` Name string `db:"name" json:"name"` X int `db:"x" json:"x"` @@ -23,11 +26,35 @@ type Town struct { ShopList string `db:"shop_list" json:"shop_list"` } -// New creates a new Town with sensible defaults +func (t *Town) GetTableName() string { + return "towns" +} + +func (t *Town) GetID() int { + return t.ID +} + +func (t *Town) SetID(id int) { + t.ID = id +} + +func (t *Town) Set(field string, value any) error { + return database.Set(t, field, value) +} + +func (t *Town) Save() error { + return database.Save(t) +} + +func (t *Town) Delete() error { + return database.Delete(t) +} + +// Creates a new Town with sensible defaults func New() *Town { return &Town{ Name: "", - X: 0, // Default coordinates + X: 0, // Default coordinates Y: 0, InnCost: 50, // Default inn cost MapCost: 100, // Default map cost @@ -38,19 +65,19 @@ func New() *Town { var townScanner = scanner.New[Town]() -// townColumns returns the column list for town queries +// Returns the column list for town queries func townColumns() string { return townScanner.Columns() } -// scanTown populates a Town struct using the fast scanner +// Populates a Town struct using the fast scanner func scanTown(stmt *sqlite.Stmt) *Town { town := &Town{} townScanner.Scan(stmt, town) return town } -// Find retrieves a town by ID +// Retrieves a town by ID func Find(id int) (*Town, error) { var town *Town @@ -72,7 +99,7 @@ func Find(id int) (*Town, error) { return town, nil } -// All retrieves all towns +// Retrieves all towns func All() ([]*Town, error) { var towns []*Town @@ -91,7 +118,7 @@ func All() ([]*Town, error) { return towns, nil } -// ByName retrieves a town by name (case-insensitive) +// Retrieves a town by name (case-insensitive) func ByName(name string) (*Town, error) { var town *Town @@ -113,7 +140,7 @@ func ByName(name string) (*Town, error) { return town, nil } -// ByMaxInnCost retrieves towns with inn cost at most the specified amount +// Retrieves towns with inn cost at most the specified amount func ByMaxInnCost(maxCost int) ([]*Town, error) { var towns []*Town @@ -132,7 +159,7 @@ func ByMaxInnCost(maxCost int) ([]*Town, error) { return towns, nil } -// ByMaxTPCost retrieves towns with teleport cost at most the specified amount +// Retrieves towns with teleport cost at most the specified amount func ByMaxTPCost(maxCost int) ([]*Town, error) { var towns []*Town @@ -151,7 +178,7 @@ func ByMaxTPCost(maxCost int) ([]*Town, error) { return towns, nil } -// ByCoords retrieves a town by its x, y coordinates +// Retrieves a town by its x, y coordinates func ByCoords(x, y int) (*Town, error) { var town *Town @@ -169,7 +196,7 @@ func ByCoords(x, y int) (*Town, error) { return town, nil } -// ByDistance retrieves towns within a certain distance from a point +// Retrieves towns within a certain distance from a point func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) { var towns []*Town @@ -192,145 +219,72 @@ func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) { return towns, nil } -// Save updates an existing town in the database -func (t *Town) Save() error { - if t.ID == 0 { - return fmt.Errorf("cannot save town without ID") - } - - query := `UPDATE towns SET name = ?, x = ?, y = ?, inn_cost = ?, map_cost = ?, tp_cost = ?, shop_list = ? WHERE id = ?` - return database.Exec(query, t.Name, t.X, t.Y, t.InnCost, t.MapCost, t.TPCost, t.ShopList, t.ID) -} - -// Insert saves a new town to the database and sets the ID +// Saves a new town to the database and sets the ID func (t *Town) Insert() error { - if t.ID != 0 { - return fmt.Errorf("town already has ID %d, use Save() to update", t.ID) - } - - // Use a transaction to ensure we can get the ID - err := database.Transaction(func(tx *database.Tx) error { - query := `INSERT INTO towns (name, x, y, inn_cost, map_cost, tp_cost, shop_list) VALUES (?, ?, ?, ?, ?, ?, ?)` - - if err := tx.Exec(query, t.Name, t.X, t.Y, t.InnCost, t.MapCost, t.TPCost, t.ShopList); err != nil { - return fmt.Errorf("failed to insert town: %w", err) - } - - // Get the last insert ID - var id int - err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error { - id = stmt.ColumnInt(0) - return nil - }) - if err != nil { - return fmt.Errorf("failed to get insert ID: %w", err) - } - - t.ID = id - return nil - }) - - return err + columns := `name, x, y, inn_cost, map_cost, tp_cost, shop_list` + values := []any{t.Name, t.X, t.Y, t.InnCost, t.MapCost, t.TPCost, t.ShopList} + return database.Insert(t, columns, values...) } -// Delete removes the town from the database -func (t *Town) Delete() error { - if t.ID == 0 { - return fmt.Errorf("cannot delete town without ID") - } - - query := "DELETE FROM towns WHERE id = ?" - return database.Exec(query, t.ID) +// Returns the shop items as a slice of item IDs +func (t *Town) GetShopItems() []int { + return helpers.StringToInts(t.ShopList) } -// GetShopItems returns the shop items as a slice of item IDs -func (t *Town) GetShopItems() []string { - if t.ShopList == "" { - return []string{} - } - return strings.Split(t.ShopList, ",") +// Sets the shop items from a slice of item IDs +func (t *Town) SetShopItems(items []int) { + t.Set("ShopList", helpers.IntsToString(items)) } -// SetShopItems sets the shop items from a slice of item IDs -func (t *Town) SetShopItems(items []string) { - t.ShopList = strings.Join(items, ",") +// Checks if the town's shop sells a specific item ID +func (t *Town) HasShopItem(itemID int) bool { + return slices.Contains(t.GetShopItems(), itemID) } -// HasShopItem checks if the town's shop sells a specific item ID -func (t *Town) HasShopItem(itemID string) bool { - items := t.GetShopItems() - for _, item := range items { - if strings.TrimSpace(item) == itemID { - return true - } - } - return false -} - -// DistanceFrom calculates the squared distance from this town to given coordinates +// Calculates the squared distance from this town to given coordinates func (t *Town) DistanceFromSquared(x, y int) float64 { dx := float64(t.X - x) dy := float64(t.Y - y) return dx*dx + dy*dy // Return squared distance for performance } -// DistanceFrom calculates the actual distance from this town to given coordinates +// Calculates the actual distance from this town to given coordinates func (t *Town) DistanceFrom(x, y int) float64 { return math.Sqrt(t.DistanceFromSquared(x, y)) } -// IsStartingTown returns true if this is the starting town (Midworld) +// Returns true if this is the starting town (Midworld) func (t *Town) IsStartingTown() bool { return t.X == 0 && t.Y == 0 } -// CanAffordInn returns true if the player can afford the inn +// Returns true if the player can afford the inn func (t *Town) CanAffordInn(gold int) bool { return gold >= t.InnCost } -// CanAffordMap returns true if the player can afford to buy the map +// Returns true if the player can afford to buy the map func (t *Town) CanAffordMap(gold int) bool { return gold >= t.MapCost } -// CanAffordTeleport returns true if the player can afford to teleport here +// Returns true if the player can afford to teleport here func (t *Town) CanAffordTeleport(gold int) bool { return gold >= t.TPCost } -// HasShop returns true if the town has a shop with items +// Returns true if the town has a shop with items func (t *Town) HasShop() bool { return len(t.GetShopItems()) > 0 } -// GetPosition returns the town's coordinates +// Returns the town's coordinates func (t *Town) GetPosition() (int, int) { return t.X, t.Y } -// SetPosition sets the town's coordinates +// Sets the town's coordinates func (t *Town) SetPosition(x, y int) { - t.X = x - t.Y = y -} - -// ToMap converts the town to a map for efficient template rendering -func (t *Town) ToMap() map[string]any { - return map[string]any{ - "ID": t.ID, - "Name": t.Name, - "X": t.X, - "Y": t.Y, - "InnCost": t.InnCost, - "MapCost": t.MapCost, - "TPCost": t.TPCost, - "ShopList": t.ShopList, - - // Computed values - "ShopItems": t.GetShopItems(), - "HasShop": t.HasShop(), - "IsStartingTown": t.IsStartingTown(), - "Position": map[string]int{"X": t.X, "Y": t.Y}, - } + t.Set("X", x) + t.Set("Y", y) } diff --git a/internal/users/users.go b/internal/users/users.go index b79e5b6..14d64fb 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -339,71 +339,3 @@ func (u *User) SetPosition(x, y int) { u.Set("X", x) u.Set("Y", y) } - -func (u *User) ToMap() map[string]any { - return map[string]any{ - "ID": u.ID, - "Username": u.Username, - "Email": u.Email, - "Verified": u.Verified, - "Token": u.Token, - "Registered": u.Registered, - "LastOnline": u.LastOnline, - "Auth": u.Auth, - "X": u.X, - "Y": u.Y, - "ClassID": u.ClassID, - "Currently": u.Currently, - "Fighting": u.Fighting, - "MonsterID": u.MonsterID, - "MonsterHP": u.MonsterHP, - "MonsterSleep": u.MonsterSleep, - "MonsterImmune": u.MonsterImmune, - "UberDamage": u.UberDamage, - "UberDefense": u.UberDefense, - "HP": u.HP, - "MP": u.MP, - "TP": u.TP, - "MaxHP": u.MaxHP, - "MaxMP": u.MaxMP, - "MaxTP": u.MaxTP, - "Level": u.Level, - "Gold": u.Gold, - "Exp": u.Exp, - "GoldBonus": u.GoldBonus, - "ExpBonus": u.ExpBonus, - "Strength": u.Strength, - "Dexterity": u.Dexterity, - "Attack": u.Attack, - "Defense": u.Defense, - "WeaponID": u.WeaponID, - "ArmorID": u.ArmorID, - "ShieldID": u.ShieldID, - "Slot1ID": u.Slot1ID, - "Slot2ID": u.Slot2ID, - "Slot3ID": u.Slot3ID, - "WeaponName": u.WeaponName, - "ArmorName": u.ArmorName, - "ShieldName": u.ShieldName, - "Slot1Name": u.Slot1Name, - "Slot2Name": u.Slot2Name, - "Slot3Name": u.Slot3Name, - "DropCode": u.DropCode, - "Spells": u.Spells, - "Towns": u.Towns, - - // Computed values - "IsVerified": u.IsVerified(), - "IsAdmin": u.IsAdmin(), - "IsModerator": u.IsModerator(), - "IsFighting": u.IsFighting(), - "IsAlive": u.IsAlive(), - "RegisteredTime": u.RegisteredTime(), - "LastOnlineTime": u.LastOnlineTime(), - "Equipment": u.GetEquipment(), - "Stats": u.GetStats(), - "Position": map[string]int{"X": u.X, "Y": u.Y}, - "SpellIDs": u.GetSpellIDs(), - "TownIDs": u.GetTownIDs(), - } -} diff --git a/templates/leftside.html b/templates/leftside.html index 93185bc..b1569d7 100644 --- a/templates/leftside.html +++ b/templates/leftside.html @@ -26,8 +26,12 @@
Towns
{if #_towns > 0} Teleport to: - {for map in _towns} - {map.name} + {for id,name in _towns} + {if town != nil and name == town.Name} + {name} (here) + {else} + {name} + {/if} {/for} {else} No town maps diff --git a/templates/rightside.html b/templates/rightside.html index 1ca4347..fcd8a7a 100644 --- a/templates/rightside.html +++ b/templates/rightside.html @@ -65,8 +65,8 @@
Fast Spells
{if #_spells > 0} - {for spell in _spells} - {spell.name} + {for id,name in _spells} + {name} {/for} {else} No known healing spells diff --git a/templates/town/shop.html b/templates/town/shop.html new file mode 100644 index 0000000..f24d5aa --- /dev/null +++ b/templates/town/shop.html @@ -0,0 +1,31 @@ +{include "layout.html"} + +{block "content"} +
+
+

{town.Name} Shop

+

Buying weapons will increase your Attack. Buying armor and shields will increase your Defense.

+

Click an item name to purchase it.

+

The following items are available at this town:

+
+ +
+ + {for item in itemlist} + + + + + {/for} +
+ {if item.Type == 1} + Weapon + {/if} + + {item.Name} +
+ +

If you've changed your mind, you may also return back to town.

+
+
+{/block} \ No newline at end of file