rework all models to new paradigm, fix ordered map to work with any interface correctly
This commit is contained in:
parent
e3a125f6cf
commit
2b86e9fa79
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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,7 +24,31 @@ 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: "",
|
||||
@ -38,12 +64,12 @@ func New() *Monster {
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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,7 +26,31 @@ 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: "",
|
||||
@ -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)
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,12 @@
|
||||
<div class="title"><img src="/assets/images/button_towns.gif" alt="Towns" title="Towns"></div>
|
||||
{if #_towns > 0}
|
||||
<i>Teleport to:</i>
|
||||
{for map in _towns}
|
||||
<a href="#">{map.name}</a>
|
||||
{for id,name in _towns}
|
||||
{if town != nil and name == town.Name}
|
||||
<span>{name} <i>(here)</i></span>
|
||||
{else}
|
||||
<a href="#">{name}</a>
|
||||
{/if}
|
||||
{/for}
|
||||
{else}
|
||||
<i>No town maps</i>
|
||||
|
@ -65,8 +65,8 @@
|
||||
<section>
|
||||
<div class="title"><img src="/assets/images/button_fastspells.gif" alt="Fast Spells" title="Fast Spells"></div>
|
||||
{if #_spells > 0}
|
||||
{for spell in _spells}
|
||||
<a href="#">{spell.name}</a>
|
||||
{for id,name in _spells}
|
||||
<a href="#">{name}</a>
|
||||
{/for}
|
||||
{else}
|
||||
<i>No known healing spells</i>
|
||||
|
31
templates/town/shop.html
Normal file
31
templates/town/shop.html
Normal file
@ -0,0 +1,31 @@
|
||||
{include "layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<div class="town shop">
|
||||
<section>
|
||||
<div class="title"><h3>{town.Name} Shop</h3></div>
|
||||
<p>Buying weapons will increase your Attack. Buying armor and shields will increase your Defense.</p>
|
||||
<p>Click an item name to purchase it.</p>
|
||||
<p>The following items are available at this town:</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<table class="item-shop">
|
||||
{for item in itemlist}
|
||||
<tr>
|
||||
<td>
|
||||
{if item.Type == 1}
|
||||
<img src="images/icon_weapon.gif" alt="Weapon" title="Weapon">
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{item.Name}
|
||||
</td>
|
||||
</tr>
|
||||
{/for}
|
||||
</table>
|
||||
|
||||
<p>If you've changed your mind, you may also return back to <a href="/town">town</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
{/block}
|
Loading…
x
Reference in New Issue
Block a user