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
|
// Babble represents a global chat message in the database
|
||||||
type Babble struct {
|
type Babble struct {
|
||||||
|
database.BaseModel
|
||||||
|
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Posted int64 `db:"posted" json:"posted"`
|
Posted int64 `db:"posted" json:"posted"`
|
||||||
Author string `db:"author" json:"author"`
|
Author string `db:"author" json:"author"`
|
||||||
Babble string `db:"babble" json:"babble"`
|
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 {
|
func New() *Babble {
|
||||||
return &Babble{
|
return &Babble{
|
||||||
Posted: time.Now().Unix(),
|
Posted: time.Now().Unix(),
|
||||||
@ -30,19 +56,19 @@ func New() *Babble {
|
|||||||
|
|
||||||
var babbleScanner = scanner.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 {
|
func babbleColumns() string {
|
||||||
return babbleScanner.Columns()
|
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 {
|
func scanBabble(stmt *sqlite.Stmt) *Babble {
|
||||||
babble := &Babble{}
|
babble := &Babble{}
|
||||||
babbleScanner.Scan(stmt, babble)
|
babbleScanner.Scan(stmt, babble)
|
||||||
return babble
|
return babble
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find retrieves a babble message by ID
|
// Retrieves a babble message by ID
|
||||||
func Find(id int) (*Babble, error) {
|
func Find(id int) (*Babble, error) {
|
||||||
var babble *Babble
|
var babble *Babble
|
||||||
|
|
||||||
@ -64,7 +90,7 @@ func Find(id int) (*Babble, error) {
|
|||||||
return babble, nil
|
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) {
|
func All() ([]*Babble, error) {
|
||||||
var babbles []*Babble
|
var babbles []*Babble
|
||||||
|
|
||||||
@ -83,7 +109,7 @@ func All() ([]*Babble, error) {
|
|||||||
return babbles, nil
|
return babbles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByAuthor retrieves babble messages by a specific author
|
// Retrieves babble messages by a specific author
|
||||||
func ByAuthor(author string) ([]*Babble, error) {
|
func ByAuthor(author string) ([]*Babble, error) {
|
||||||
var babbles []*Babble
|
var babbles []*Babble
|
||||||
|
|
||||||
@ -102,7 +128,7 @@ func ByAuthor(author string) ([]*Babble, error) {
|
|||||||
return babbles, nil
|
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) {
|
func Recent(limit int) ([]*Babble, error) {
|
||||||
var babbles []*Babble
|
var babbles []*Babble
|
||||||
|
|
||||||
@ -121,7 +147,7 @@ func Recent(limit int) ([]*Babble, error) {
|
|||||||
return babbles, nil
|
return babbles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since retrieves babble messages since a specific timestamp
|
// Retrieves babble messages since a specific timestamp
|
||||||
func Since(since int64) ([]*Babble, error) {
|
func Since(since int64) ([]*Babble, error) {
|
||||||
var babbles []*Babble
|
var babbles []*Babble
|
||||||
|
|
||||||
@ -140,7 +166,7 @@ func Since(since int64) ([]*Babble, error) {
|
|||||||
return babbles, nil
|
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) {
|
func Between(start, end int64) ([]*Babble, error) {
|
||||||
var babbles []*Babble
|
var babbles []*Babble
|
||||||
|
|
||||||
@ -159,7 +185,7 @@ func Between(start, end int64) ([]*Babble, error) {
|
|||||||
return babbles, nil
|
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) {
|
func Search(term string) ([]*Babble, error) {
|
||||||
var babbles []*Babble
|
var babbles []*Babble
|
||||||
|
|
||||||
@ -179,7 +205,7 @@ func Search(term string) ([]*Babble, error) {
|
|||||||
return babbles, nil
|
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) {
|
func RecentByAuthor(author string, limit int) ([]*Babble, error) {
|
||||||
var babbles []*Babble
|
var babbles []*Babble
|
||||||
|
|
||||||
@ -198,82 +224,39 @@ func RecentByAuthor(author string, limit int) ([]*Babble, error) {
|
|||||||
return babbles, nil
|
return babbles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updates an existing babble message in the database
|
// Saves a new babble to the database and sets the ID
|
||||||
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
|
|
||||||
func (b *Babble) Insert() error {
|
func (b *Babble) Insert() error {
|
||||||
if b.ID != 0 {
|
columns := `posted, author, babble`
|
||||||
return fmt.Errorf("babble already has ID %d, use Save() to update", b.ID)
|
values := []any{b.Posted, b.Author, b.Babble}
|
||||||
}
|
return database.Insert(b, columns, values...)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the babble message from the database
|
// Returns the posted timestamp as a time.Time
|
||||||
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
|
|
||||||
func (b *Babble) PostedTime() time.Time {
|
func (b *Babble) PostedTime() time.Time {
|
||||||
return time.Unix(b.Posted, 0)
|
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) {
|
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 {
|
func (b *Babble) IsRecent() bool {
|
||||||
return time.Since(b.PostedTime()) < time.Hour
|
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 {
|
func (b *Babble) Age() time.Duration {
|
||||||
return time.Since(b.PostedTime())
|
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 {
|
func (b *Babble) IsAuthor(username string) bool {
|
||||||
return strings.EqualFold(b.Author, username)
|
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 {
|
func (b *Babble) Preview(maxLength int) string {
|
||||||
if len(b.Babble) <= maxLength {
|
if len(b.Babble) <= maxLength {
|
||||||
return b.Babble
|
return b.Babble
|
||||||
@ -286,7 +269,7 @@ func (b *Babble) Preview(maxLength int) string {
|
|||||||
return b.Babble[:maxLength-3] + "..."
|
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 {
|
func (b *Babble) WordCount() int {
|
||||||
if b.Babble == "" {
|
if b.Babble == "" {
|
||||||
return 0
|
return 0
|
||||||
@ -314,27 +297,27 @@ func (b *Babble) WordCount() int {
|
|||||||
return words
|
return words
|
||||||
}
|
}
|
||||||
|
|
||||||
// Length returns the character length of the babble message
|
// Returns the character length of the babble message
|
||||||
func (b *Babble) Length() int {
|
func (b *Babble) Length() int {
|
||||||
return len(b.Babble)
|
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 {
|
func (b *Babble) Contains(term string) bool {
|
||||||
return strings.Contains(strings.ToLower(b.Babble), strings.ToLower(term))
|
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 {
|
func (b *Babble) IsEmpty() bool {
|
||||||
return strings.TrimSpace(b.Babble) == ""
|
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 {
|
func (b *Babble) IsLongMessage(threshold int) bool {
|
||||||
return b.Length() > threshold
|
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 {
|
func (b *Babble) GetMentions() []string {
|
||||||
words := strings.Fields(b.Babble)
|
words := strings.Fields(b.Babble)
|
||||||
var mentions []string
|
var mentions []string
|
||||||
@ -352,7 +335,7 @@ func (b *Babble) GetMentions() []string {
|
|||||||
return mentions
|
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 {
|
func (b *Babble) HasMention(username string) bool {
|
||||||
mentions := b.GetMentions()
|
mentions := b.GetMentions()
|
||||||
for _, mention := range mentions {
|
for _, mention := range mentions {
|
||||||
@ -362,22 +345,3 @@ func (b *Babble) HasMention(username string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
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
|
// Control represents the game control settings in the database
|
||||||
type Control struct {
|
type Control struct {
|
||||||
|
database.BaseModel
|
||||||
|
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
WorldSize int `db:"world_size" json:"world_size"`
|
WorldSize int `db:"world_size" json:"world_size"`
|
||||||
Open int `db:"open" json:"open"`
|
Open int `db:"open" json:"open"`
|
||||||
@ -20,7 +22,31 @@ type Control struct {
|
|||||||
Class3Name string `db:"class_3_name" json:"class_3_name"`
|
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 {
|
func New() *Control {
|
||||||
return &Control{
|
return &Control{
|
||||||
WorldSize: 200, // Default world size
|
WorldSize: 200, // Default world size
|
||||||
@ -34,19 +60,19 @@ func New() *Control {
|
|||||||
|
|
||||||
var controlScanner = scanner.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 {
|
func controlColumns() string {
|
||||||
return controlScanner.Columns()
|
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 {
|
func scanControl(stmt *sqlite.Stmt) *Control {
|
||||||
control := &Control{}
|
control := &Control{}
|
||||||
controlScanner.Scan(stmt, control)
|
controlScanner.Scan(stmt, control)
|
||||||
return 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) {
|
func Find(id int) (*Control, error) {
|
||||||
var control *Control
|
var control *Control
|
||||||
|
|
||||||
@ -68,86 +94,43 @@ func Find(id int) (*Control, error) {
|
|||||||
return control, nil
|
return control, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves the main control record (ID 1)
|
// Retrieves the main control record (ID 1)
|
||||||
func Get() (*Control, error) {
|
func Get() (*Control, error) {
|
||||||
return Find(1)
|
return Find(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updates the control record in the database
|
// Saves a new control to the database and sets the ID
|
||||||
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
|
|
||||||
func (c *Control) Insert() error {
|
func (c *Control) Insert() error {
|
||||||
if c.ID != 0 {
|
columns := `world_size, open, admin_email, class_1_name, class_2_name, class_3_name`
|
||||||
return fmt.Errorf("control already has ID %d, use Save() to update", c.ID)
|
values := []any{c.WorldSize, c.Open, c.AdminEmail, c.Class1Name, c.Class2Name, c.Class3Name}
|
||||||
}
|
return database.Insert(c, columns, values...)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the control record from the database
|
// Returns true if the game world is open for new players
|
||||||
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
|
|
||||||
func (c *Control) IsOpen() bool {
|
func (c *Control) IsOpen() bool {
|
||||||
return c.Open == 1
|
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) {
|
func (c *Control) SetOpen(open bool) {
|
||||||
if open {
|
if open {
|
||||||
c.Open = 1
|
c.Set("Open", 1)
|
||||||
} else {
|
} 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() {
|
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() {
|
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 {
|
func (c *Control) GetClassNames() []string {
|
||||||
classes := make([]string, 0, 3)
|
classes := make([]string, 0, 3)
|
||||||
if c.Class1Name != "" {
|
if c.Class1Name != "" {
|
||||||
@ -162,26 +145,26 @@ func (c *Control) GetClassNames() []string {
|
|||||||
return classes
|
return classes
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetClassNames sets all class names from a slice
|
// Sets all class names from a slice
|
||||||
func (c *Control) SetClassNames(classes []string) {
|
func (c *Control) SetClassNames(classes []string) {
|
||||||
// Reset all class names
|
// Reset all class names
|
||||||
c.Class1Name = ""
|
c.Set("Class1Name", "")
|
||||||
c.Class2Name = ""
|
c.Set("Class2Name", "")
|
||||||
c.Class3Name = ""
|
c.Set("Class3Name", "")
|
||||||
|
|
||||||
// Set provided class names
|
// Set provided class names
|
||||||
if len(classes) > 0 {
|
if len(classes) > 0 {
|
||||||
c.Class1Name = classes[0]
|
c.Set("Class1Name", classes[0])
|
||||||
}
|
}
|
||||||
if len(classes) > 1 {
|
if len(classes) > 1 {
|
||||||
c.Class2Name = classes[1]
|
c.Set("Class2Name", classes[1])
|
||||||
}
|
}
|
||||||
if len(classes) > 2 {
|
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 {
|
func (c *Control) GetClassName(classNum int) string {
|
||||||
switch classNum {
|
switch classNum {
|
||||||
case 1:
|
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 {
|
func (c *Control) SetClassName(classNum int, name string) bool {
|
||||||
switch classNum {
|
switch classNum {
|
||||||
case 1:
|
case 1:
|
||||||
c.Class1Name = name
|
c.Set("Class1Name", name)
|
||||||
return true
|
return true
|
||||||
case 2:
|
case 2:
|
||||||
c.Class2Name = name
|
c.Set("Class2Name", name)
|
||||||
return true
|
return true
|
||||||
case 3:
|
case 3:
|
||||||
c.Class3Name = name
|
c.Set("Class3Name", name)
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
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 {
|
func (c *Control) IsValidClassName(name string) bool {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return false
|
return false
|
||||||
@ -220,7 +203,7 @@ func (c *Control) IsValidClassName(name string) bool {
|
|||||||
return name == c.Class1Name || name == c.Class2Name || name == c.Class3Name
|
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 {
|
func (c *Control) GetClassNumber(name string) int {
|
||||||
if name == c.Class1Name && name != "" {
|
if name == c.Class1Name && name != "" {
|
||||||
return 1
|
return 1
|
||||||
@ -234,55 +217,29 @@ func (c *Control) GetClassNumber(name string) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasAdminEmail returns true if an admin email is configured
|
// Returns true if an admin email is configured
|
||||||
func (c *Control) HasAdminEmail() bool {
|
func (c *Control) HasAdminEmail() bool {
|
||||||
return c.AdminEmail != ""
|
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 {
|
func (c *Control) IsWorldSizeValid() bool {
|
||||||
return c.WorldSize > 0 && c.WorldSize <= 10000
|
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 {
|
func (c *Control) GetWorldRadius() int {
|
||||||
return c.WorldSize / 2
|
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 {
|
func (c *Control) IsWithinWorldBounds(x, y int) bool {
|
||||||
radius := c.GetWorldRadius()
|
radius := c.GetWorldRadius()
|
||||||
return x >= -radius && x <= radius && y >= -radius && y <= radius
|
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) {
|
func (c *Control) GetWorldBounds() (minX, minY, maxX, maxY int) {
|
||||||
radius := c.GetWorldRadius()
|
radius := c.GetWorldRadius()
|
||||||
return -radius, -radius, radius, radius
|
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
|
// Drop represents a drop item in the database
|
||||||
type Drop struct {
|
type Drop struct {
|
||||||
|
database.BaseModel
|
||||||
|
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
Level int `db:"level" json:"level"`
|
Level int `db:"level" json:"level"`
|
||||||
@ -18,7 +20,31 @@ type Drop struct {
|
|||||||
Att string `db:"att" json:"att"`
|
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 {
|
func New() *Drop {
|
||||||
return &Drop{
|
return &Drop{
|
||||||
Name: "",
|
Name: "",
|
||||||
@ -30,12 +56,12 @@ func New() *Drop {
|
|||||||
|
|
||||||
var dropScanner = scanner.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 {
|
func dropColumns() string {
|
||||||
return dropScanner.Columns()
|
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 {
|
func scanDrop(stmt *sqlite.Stmt) *Drop {
|
||||||
drop := &Drop{}
|
drop := &Drop{}
|
||||||
dropScanner.Scan(stmt, drop)
|
dropScanner.Scan(stmt, drop)
|
||||||
@ -47,7 +73,7 @@ const (
|
|||||||
TypeConsumable = 1
|
TypeConsumable = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// Find retrieves a drop by ID
|
// Retrieves a drop by ID
|
||||||
func Find(id int) (*Drop, error) {
|
func Find(id int) (*Drop, error) {
|
||||||
var drop *Drop
|
var drop *Drop
|
||||||
|
|
||||||
@ -69,7 +95,7 @@ func Find(id int) (*Drop, error) {
|
|||||||
return drop, nil
|
return drop, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// All retrieves all drops
|
// Retrieves all drops
|
||||||
func All() ([]*Drop, error) {
|
func All() ([]*Drop, error) {
|
||||||
var drops []*Drop
|
var drops []*Drop
|
||||||
|
|
||||||
@ -88,7 +114,7 @@ func All() ([]*Drop, error) {
|
|||||||
return drops, nil
|
return drops, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByLevel retrieves drops by minimum level requirement
|
// Retrieves drops by minimum level requirement
|
||||||
func ByLevel(minLevel int) ([]*Drop, error) {
|
func ByLevel(minLevel int) ([]*Drop, error) {
|
||||||
var drops []*Drop
|
var drops []*Drop
|
||||||
|
|
||||||
@ -107,7 +133,7 @@ func ByLevel(minLevel int) ([]*Drop, error) {
|
|||||||
return drops, nil
|
return drops, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByType retrieves drops by type
|
// Retrieves drops by type
|
||||||
func ByType(dropType int) ([]*Drop, error) {
|
func ByType(dropType int) ([]*Drop, error) {
|
||||||
var drops []*Drop
|
var drops []*Drop
|
||||||
|
|
||||||
@ -126,62 +152,19 @@ func ByType(dropType int) ([]*Drop, error) {
|
|||||||
return drops, nil
|
return drops, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updates an existing drop in the database
|
// Saves a new drop to the database and sets the ID
|
||||||
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
|
|
||||||
func (d *Drop) Insert() error {
|
func (d *Drop) Insert() error {
|
||||||
if d.ID != 0 {
|
columns := `name, level, type, att`
|
||||||
return fmt.Errorf("drop already has ID %d, use Save() to update", d.ID)
|
values := []any{d.Name, d.Level, d.Type, d.Att}
|
||||||
}
|
return database.Insert(d, columns, values...)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the drop from the database
|
// Returns true if the drop is a consumable item
|
||||||
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
|
|
||||||
func (d *Drop) IsConsumable() bool {
|
func (d *Drop) IsConsumable() bool {
|
||||||
return d.Type == TypeConsumable
|
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 {
|
func (d *Drop) TypeName() string {
|
||||||
switch d.Type {
|
switch d.Type {
|
||||||
case TypeConsumable:
|
case TypeConsumable:
|
||||||
@ -190,18 +173,3 @@ func (d *Drop) TypeName() string {
|
|||||||
return "Unknown"
|
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
|
// Forum represents a forum post or thread in the database
|
||||||
type Forum struct {
|
type Forum struct {
|
||||||
|
database.BaseModel
|
||||||
|
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Posted int64 `db:"posted" json:"posted"`
|
Posted int64 `db:"posted" json:"posted"`
|
||||||
LastPost int64 `db:"last_post" json:"last_post"`
|
LastPost int64 `db:"last_post" json:"last_post"`
|
||||||
@ -23,7 +25,31 @@ type Forum struct {
|
|||||||
Content string `db:"content" json:"content"`
|
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 {
|
func New() *Forum {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
return &Forum{
|
return &Forum{
|
||||||
@ -39,19 +65,19 @@ func New() *Forum {
|
|||||||
|
|
||||||
var forumScanner = scanner.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 {
|
func forumColumns() string {
|
||||||
return forumScanner.Columns()
|
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 {
|
func scanForum(stmt *sqlite.Stmt) *Forum {
|
||||||
forum := &Forum{}
|
forum := &Forum{}
|
||||||
forumScanner.Scan(stmt, forum)
|
forumScanner.Scan(stmt, forum)
|
||||||
return forum
|
return forum
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find retrieves a forum post by ID
|
// Retrieves a forum post by ID
|
||||||
func Find(id int) (*Forum, error) {
|
func Find(id int) (*Forum, error) {
|
||||||
var forum *Forum
|
var forum *Forum
|
||||||
|
|
||||||
@ -73,7 +99,7 @@ func Find(id int) (*Forum, error) {
|
|||||||
return forum, nil
|
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) {
|
func All() ([]*Forum, error) {
|
||||||
var forums []*Forum
|
var forums []*Forum
|
||||||
|
|
||||||
@ -92,7 +118,7 @@ func All() ([]*Forum, error) {
|
|||||||
return forums, nil
|
return forums, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Threads retrieves all top-level forum threads (parent = 0)
|
// Retrieves all top-level forum threads (parent = 0)
|
||||||
func Threads() ([]*Forum, error) {
|
func Threads() ([]*Forum, error) {
|
||||||
var forums []*Forum
|
var forums []*Forum
|
||||||
|
|
||||||
@ -111,7 +137,7 @@ func Threads() ([]*Forum, error) {
|
|||||||
return forums, nil
|
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) {
|
func ByParent(parentID int) ([]*Forum, error) {
|
||||||
var forums []*Forum
|
var forums []*Forum
|
||||||
|
|
||||||
@ -130,7 +156,7 @@ func ByParent(parentID int) ([]*Forum, error) {
|
|||||||
return forums, nil
|
return forums, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByAuthor retrieves forum posts by a specific author
|
// Retrieves forum posts by a specific author
|
||||||
func ByAuthor(authorID int) ([]*Forum, error) {
|
func ByAuthor(authorID int) ([]*Forum, error) {
|
||||||
var forums []*Forum
|
var forums []*Forum
|
||||||
|
|
||||||
@ -149,7 +175,7 @@ func ByAuthor(authorID int) ([]*Forum, error) {
|
|||||||
return forums, nil
|
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) {
|
func Recent(limit int) ([]*Forum, error) {
|
||||||
var forums []*Forum
|
var forums []*Forum
|
||||||
|
|
||||||
@ -168,7 +194,7 @@ func Recent(limit int) ([]*Forum, error) {
|
|||||||
return forums, nil
|
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) {
|
func Search(term string) ([]*Forum, error) {
|
||||||
var forums []*Forum
|
var forums []*Forum
|
||||||
|
|
||||||
@ -188,7 +214,7 @@ func Search(term string) ([]*Forum, error) {
|
|||||||
return forums, nil
|
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) {
|
func Since(since int64) ([]*Forum, error) {
|
||||||
var forums []*Forum
|
var forums []*Forum
|
||||||
|
|
||||||
@ -207,112 +233,69 @@ func Since(since int64) ([]*Forum, error) {
|
|||||||
return forums, nil
|
return forums, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updates an existing forum post in the database
|
// Saves a new forum post to the database and sets the ID
|
||||||
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
|
|
||||||
func (f *Forum) Insert() error {
|
func (f *Forum) Insert() error {
|
||||||
if f.ID != 0 {
|
columns := `posted, last_post, author, parent, replies, title, content`
|
||||||
return fmt.Errorf("forum post already has ID %d, use Save() to update", f.ID)
|
values := []any{f.Posted, f.LastPost, f.Author, f.Parent, f.Replies, f.Title, f.Content}
|
||||||
}
|
return database.Insert(f, columns, values...)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the forum post from the database
|
// Returns the posted timestamp as a time.Time
|
||||||
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
|
|
||||||
func (f *Forum) PostedTime() time.Time {
|
func (f *Forum) PostedTime() time.Time {
|
||||||
return time.Unix(f.Posted, 0)
|
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 {
|
func (f *Forum) LastPostTime() time.Time {
|
||||||
return time.Unix(f.LastPost, 0)
|
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) {
|
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) {
|
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 {
|
func (f *Forum) IsThread() bool {
|
||||||
return f.Parent == 0
|
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 {
|
func (f *Forum) IsReply() bool {
|
||||||
return f.Parent > 0
|
return f.Parent > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasReplies returns true if this post has replies
|
// Returns true if this post has replies
|
||||||
func (f *Forum) HasReplies() bool {
|
func (f *Forum) HasReplies() bool {
|
||||||
return f.Replies > 0
|
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 {
|
func (f *Forum) IsRecentActivity() bool {
|
||||||
return time.Since(f.LastPostTime()) < 24*time.Hour
|
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 {
|
func (f *Forum) ActivityAge() time.Duration {
|
||||||
return time.Since(f.LastPostTime())
|
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 {
|
func (f *Forum) PostAge() time.Duration {
|
||||||
return time.Since(f.PostedTime())
|
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 {
|
func (f *Forum) IsAuthor(userID int) bool {
|
||||||
return f.Author == userID
|
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 {
|
func (f *Forum) Preview(maxLength int) string {
|
||||||
if len(f.Content) <= maxLength {
|
if len(f.Content) <= maxLength {
|
||||||
return f.Content
|
return f.Content
|
||||||
@ -325,7 +308,7 @@ func (f *Forum) Preview(maxLength int) string {
|
|||||||
return f.Content[:maxLength-3] + "..."
|
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 {
|
func (f *Forum) WordCount() int {
|
||||||
if f.Content == "" {
|
if f.Content == "" {
|
||||||
return 0
|
return 0
|
||||||
@ -353,70 +336,44 @@ func (f *Forum) WordCount() int {
|
|||||||
return words
|
return words
|
||||||
}
|
}
|
||||||
|
|
||||||
// Length returns the character length of the content
|
// Returns the character length of the content
|
||||||
func (f *Forum) Length() int {
|
func (f *Forum) Length() int {
|
||||||
return len(f.Content)
|
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 {
|
func (f *Forum) Contains(term string) bool {
|
||||||
lowerTerm := strings.ToLower(term)
|
lowerTerm := strings.ToLower(term)
|
||||||
return strings.Contains(strings.ToLower(f.Title), lowerTerm) ||
|
return strings.Contains(strings.ToLower(f.Title), lowerTerm) ||
|
||||||
strings.Contains(strings.ToLower(f.Content), 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() {
|
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() {
|
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() {
|
func (f *Forum) DecrementReplies() {
|
||||||
if f.Replies > 0 {
|
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) {
|
func (f *Forum) GetReplies() ([]*Forum, error) {
|
||||||
return ByParent(f.ID)
|
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) {
|
func (f *Forum) GetThread() (*Forum, error) {
|
||||||
if f.IsThread() {
|
if f.IsThread() {
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
return Find(f.Parent)
|
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 {
|
func (om *OrderedMap[K, V]) ToSlice() []V {
|
||||||
result := make([]map[string]any, 0, len(om.keys))
|
result := make([]V, 0, len(om.keys))
|
||||||
for _, key := range om.keys {
|
for _, key := range om.keys {
|
||||||
result = append(result, map[string]any{
|
result = append(result, om.data[key])
|
||||||
"id": key,
|
|
||||||
"name": om.data[key],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ import (
|
|||||||
|
|
||||||
// Item represents an item in the database
|
// Item represents an item in the database
|
||||||
type Item struct {
|
type Item struct {
|
||||||
|
database.BaseModel
|
||||||
|
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Type int `db:"type" json:"type"`
|
Type int `db:"type" json:"type"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
@ -19,7 +21,31 @@ type Item struct {
|
|||||||
Special string `db:"special" json:"special"`
|
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 {
|
func New() *Item {
|
||||||
return &Item{
|
return &Item{
|
||||||
Type: TypeWeapon, // Default to weapon
|
Type: TypeWeapon, // Default to weapon
|
||||||
@ -32,12 +58,12 @@ func New() *Item {
|
|||||||
|
|
||||||
var itemScanner = scanner.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 {
|
func itemColumns() string {
|
||||||
return itemScanner.Columns()
|
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 {
|
func scanItem(stmt *sqlite.Stmt) *Item {
|
||||||
item := &Item{}
|
item := &Item{}
|
||||||
itemScanner.Scan(stmt, item)
|
itemScanner.Scan(stmt, item)
|
||||||
@ -51,7 +77,7 @@ const (
|
|||||||
TypeShield = 3
|
TypeShield = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// Find retrieves an item by ID
|
// Retrieves an item by ID
|
||||||
func Find(id int) (*Item, error) {
|
func Find(id int) (*Item, error) {
|
||||||
var item *Item
|
var item *Item
|
||||||
|
|
||||||
@ -73,7 +99,7 @@ func Find(id int) (*Item, error) {
|
|||||||
return item, nil
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// All retrieves all items
|
// Retrieves all items
|
||||||
func All() ([]*Item, error) {
|
func All() ([]*Item, error) {
|
||||||
var items []*Item
|
var items []*Item
|
||||||
|
|
||||||
@ -92,7 +118,7 @@ func All() ([]*Item, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByType retrieves items by type
|
// Retrieves items by type
|
||||||
func ByType(itemType int) ([]*Item, error) {
|
func ByType(itemType int) ([]*Item, error) {
|
||||||
var items []*Item
|
var items []*Item
|
||||||
|
|
||||||
@ -111,73 +137,29 @@ func ByType(itemType int) ([]*Item, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updates an existing item in the database
|
// Saves a new item to the database and sets the ID
|
||||||
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
|
|
||||||
func (i *Item) Insert() error {
|
func (i *Item) Insert() error {
|
||||||
if i.ID != 0 {
|
columns := `type, name, value, att, special`
|
||||||
return fmt.Errorf("item already has ID %d, use Save() to update", i.ID)
|
values := []any{i.Type, i.Name, i.Value, i.Att, i.Special}
|
||||||
}
|
return database.Insert(i, columns, values...)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the item from the database
|
// Returns true if the item is a weapon
|
||||||
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
|
|
||||||
func (i *Item) IsWeapon() bool {
|
func (i *Item) IsWeapon() bool {
|
||||||
return i.Type == TypeWeapon
|
return i.Type == TypeWeapon
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsArmor returns true if the item is armor
|
// Returns true if the item is armor
|
||||||
func (i *Item) IsArmor() bool {
|
func (i *Item) IsArmor() bool {
|
||||||
return i.Type == TypeArmor
|
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 {
|
func (i *Item) IsShield() bool {
|
||||||
return i.Type == TypeShield
|
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 {
|
func (i *Item) TypeName() string {
|
||||||
switch i.Type {
|
switch i.Type {
|
||||||
case TypeWeapon:
|
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 {
|
func (i *Item) HasSpecial() bool {
|
||||||
return i.Special != ""
|
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 {
|
func (i *Item) IsEquippable() bool {
|
||||||
return i.Type == TypeWeapon || i.Type == TypeArmor || i.Type == TypeShield
|
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
|
// Monster represents a monster in the database
|
||||||
type Monster struct {
|
type Monster struct {
|
||||||
|
database.BaseModel
|
||||||
|
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
MaxHP int `db:"max_hp" json:"max_hp"`
|
MaxHP int `db:"max_hp" json:"max_hp"`
|
||||||
@ -22,28 +24,52 @@ type Monster struct {
|
|||||||
Immune int `db:"immune" json:"immune"`
|
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 {
|
func New() *Monster {
|
||||||
return &Monster{
|
return &Monster{
|
||||||
Name: "",
|
Name: "",
|
||||||
MaxHP: 10, // Default HP
|
MaxHP: 10, // Default HP
|
||||||
MaxDmg: 5, // Default damage
|
MaxDmg: 5, // Default damage
|
||||||
Armor: 0, // Default armor
|
Armor: 0, // Default armor
|
||||||
Level: 1, // Default level
|
Level: 1, // Default level
|
||||||
MaxExp: 10, // Default exp reward
|
MaxExp: 10, // Default exp reward
|
||||||
MaxGold: 5, // Default gold reward
|
MaxGold: 5, // Default gold reward
|
||||||
Immune: ImmuneNone, // No immunity by default
|
Immune: ImmuneNone, // No immunity by default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var monsterScanner = scanner.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 {
|
func monsterColumns() string {
|
||||||
return monsterScanner.Columns()
|
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 {
|
func scanMonster(stmt *sqlite.Stmt) *Monster {
|
||||||
monster := &Monster{}
|
monster := &Monster{}
|
||||||
monsterScanner.Scan(stmt, monster)
|
monsterScanner.Scan(stmt, monster)
|
||||||
@ -57,7 +83,7 @@ const (
|
|||||||
ImmuneSleep = 2 // Immune to Sleep spells
|
ImmuneSleep = 2 // Immune to Sleep spells
|
||||||
)
|
)
|
||||||
|
|
||||||
// Find retrieves a monster by ID
|
// Retrieves a monster by ID
|
||||||
func Find(id int) (*Monster, error) {
|
func Find(id int) (*Monster, error) {
|
||||||
var monster *Monster
|
var monster *Monster
|
||||||
|
|
||||||
@ -79,7 +105,7 @@ func Find(id int) (*Monster, error) {
|
|||||||
return monster, nil
|
return monster, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// All retrieves all monsters
|
// Retrieves all monsters
|
||||||
func All() ([]*Monster, error) {
|
func All() ([]*Monster, error) {
|
||||||
var monsters []*Monster
|
var monsters []*Monster
|
||||||
|
|
||||||
@ -98,7 +124,7 @@ func All() ([]*Monster, error) {
|
|||||||
return monsters, nil
|
return monsters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByLevel retrieves monsters by level
|
// Retrieves monsters by level
|
||||||
func ByLevel(level int) ([]*Monster, error) {
|
func ByLevel(level int) ([]*Monster, error) {
|
||||||
var monsters []*Monster
|
var monsters []*Monster
|
||||||
|
|
||||||
@ -117,7 +143,7 @@ func ByLevel(level int) ([]*Monster, error) {
|
|||||||
return monsters, nil
|
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) {
|
func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) {
|
||||||
var monsters []*Monster
|
var monsters []*Monster
|
||||||
|
|
||||||
@ -136,7 +162,7 @@ func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) {
|
|||||||
return monsters, nil
|
return monsters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByImmunity retrieves monsters by immunity type
|
// Retrieves monsters by immunity type
|
||||||
func ByImmunity(immunityType int) ([]*Monster, error) {
|
func ByImmunity(immunityType int) ([]*Monster, error) {
|
||||||
var monsters []*Monster
|
var monsters []*Monster
|
||||||
|
|
||||||
@ -155,73 +181,29 @@ func ByImmunity(immunityType int) ([]*Monster, error) {
|
|||||||
return monsters, nil
|
return monsters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updates an existing monster in the database
|
// Saves a new monster to the database and sets the ID
|
||||||
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
|
|
||||||
func (m *Monster) Insert() error {
|
func (m *Monster) Insert() error {
|
||||||
if m.ID != 0 {
|
columns := `name, max_hp, max_dmg, armor, level, max_exp, max_gold, immune`
|
||||||
return fmt.Errorf("monster already has ID %d, use Save() to update", m.ID)
|
values := []any{m.Name, m.MaxHP, m.MaxDmg, m.Armor, m.Level, m.MaxExp, m.MaxGold, m.Immune}
|
||||||
}
|
return database.Insert(m, columns, values...)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the monster from the database
|
// Returns true if the monster is immune to Hurt spells
|
||||||
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
|
|
||||||
func (m *Monster) IsHurtImmune() bool {
|
func (m *Monster) IsHurtImmune() bool {
|
||||||
return m.Immune == ImmuneHurt
|
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 {
|
func (m *Monster) IsSleepImmune() bool {
|
||||||
return m.Immune == ImmuneSleep
|
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 {
|
func (m *Monster) HasImmunity() bool {
|
||||||
return m.Immune != ImmuneNone
|
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 {
|
func (m *Monster) ImmunityName() string {
|
||||||
switch m.Immune {
|
switch m.Immune {
|
||||||
case ImmuneNone:
|
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 {
|
func (m *Monster) DifficultyRating() float64 {
|
||||||
// Simple formula: (HP + Damage + Armor) / Level
|
// Simple formula: (HP + Damage + Armor) / Level
|
||||||
// Higher values indicate tougher monsters relative to their 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)
|
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 {
|
func (m *Monster) ExpPerHP() float64 {
|
||||||
if m.MaxHP == 0 {
|
if m.MaxHP == 0 {
|
||||||
return 0
|
return 0
|
||||||
@ -253,34 +235,10 @@ func (m *Monster) ExpPerHP() float64 {
|
|||||||
return float64(m.MaxExp) / float64(m.MaxHP)
|
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 {
|
func (m *Monster) GoldPerHP() float64 {
|
||||||
if m.MaxHP == 0 {
|
if m.MaxHP == 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return float64(m.MaxGold) / float64(m.MaxHP)
|
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
|
// News represents a news post in the database
|
||||||
type News struct {
|
type News struct {
|
||||||
|
database.BaseModel
|
||||||
|
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Author int `db:"author" json:"author"`
|
Author int `db:"author" json:"author"`
|
||||||
Posted int64 `db:"posted" json:"posted"`
|
Posted int64 `db:"posted" json:"posted"`
|
||||||
Content string `db:"content" json:"content"`
|
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 {
|
func New() *News {
|
||||||
return &News{
|
return &News{
|
||||||
Author: 0, // No author by default
|
Author: 0, // No author by default
|
||||||
@ -29,19 +55,19 @@ func New() *News {
|
|||||||
|
|
||||||
var newsScanner = scanner.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 {
|
func newsColumns() string {
|
||||||
return newsScanner.Columns()
|
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 {
|
func scanNews(stmt *sqlite.Stmt) *News {
|
||||||
news := &News{}
|
news := &News{}
|
||||||
newsScanner.Scan(stmt, news)
|
newsScanner.Scan(stmt, news)
|
||||||
return news
|
return news
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find retrieves a news post by ID
|
// Retrieves a news post by ID
|
||||||
func Find(id int) (*News, error) {
|
func Find(id int) (*News, error) {
|
||||||
var news *News
|
var news *News
|
||||||
|
|
||||||
@ -63,7 +89,7 @@ func Find(id int) (*News, error) {
|
|||||||
return news, nil
|
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) {
|
func All() ([]*News, error) {
|
||||||
var newsPosts []*News
|
var newsPosts []*News
|
||||||
|
|
||||||
@ -82,7 +108,7 @@ func All() ([]*News, error) {
|
|||||||
return newsPosts, nil
|
return newsPosts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByAuthor retrieves news posts by a specific author
|
// Retrieves news posts by a specific author
|
||||||
func ByAuthor(authorID int) ([]*News, error) {
|
func ByAuthor(authorID int) ([]*News, error) {
|
||||||
var newsPosts []*News
|
var newsPosts []*News
|
||||||
|
|
||||||
@ -101,7 +127,7 @@ func ByAuthor(authorID int) ([]*News, error) {
|
|||||||
return newsPosts, nil
|
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) {
|
func Recent(limit int) ([]*News, error) {
|
||||||
var newsPosts []*News
|
var newsPosts []*News
|
||||||
|
|
||||||
@ -120,7 +146,7 @@ func Recent(limit int) ([]*News, error) {
|
|||||||
return newsPosts, nil
|
return newsPosts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since retrieves news posts since a specific timestamp
|
// Retrieves news posts since a specific timestamp
|
||||||
func Since(since int64) ([]*News, error) {
|
func Since(since int64) ([]*News, error) {
|
||||||
var newsPosts []*News
|
var newsPosts []*News
|
||||||
|
|
||||||
@ -139,7 +165,7 @@ func Since(since int64) ([]*News, error) {
|
|||||||
return newsPosts, nil
|
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) {
|
func Between(start, end int64) ([]*News, error) {
|
||||||
var newsPosts []*News
|
var newsPosts []*News
|
||||||
|
|
||||||
@ -158,88 +184,44 @@ func Between(start, end int64) ([]*News, error) {
|
|||||||
return newsPosts, nil
|
return newsPosts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updates an existing news post in the database
|
// Saves a new news post to the database and sets the ID
|
||||||
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
|
|
||||||
func (n *News) Insert() error {
|
func (n *News) Insert() error {
|
||||||
if n.ID != 0 {
|
columns := `author, posted, content`
|
||||||
return fmt.Errorf("news already has ID %d, use Save() to update", n.ID)
|
values := []any{n.Author, n.Posted, n.Content}
|
||||||
}
|
return database.Insert(n, columns, values...)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the news post from the database
|
// Returns the posted timestamp as a time.Time
|
||||||
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
|
|
||||||
func (n *News) PostedTime() time.Time {
|
func (n *News) PostedTime() time.Time {
|
||||||
return time.Unix(n.Posted, 0)
|
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) {
|
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 {
|
func (n *News) IsRecent() bool {
|
||||||
return time.Since(n.PostedTime()) < 24*time.Hour
|
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 {
|
func (n *News) Age() time.Duration {
|
||||||
return time.Since(n.PostedTime())
|
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 {
|
func (n *News) ReadableTime() string {
|
||||||
return n.PostedTime().Format("Jan 2, 2006 3:04 PM")
|
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 {
|
func (n *News) IsAuthor(userID int) bool {
|
||||||
return n.Author == userID
|
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 {
|
func (n *News) Preview(maxLength int) string {
|
||||||
if len(n.Content) <= maxLength {
|
if len(n.Content) <= maxLength {
|
||||||
return n.Content
|
return n.Content
|
||||||
@ -252,7 +234,7 @@ func (n *News) Preview(maxLength int) string {
|
|||||||
return n.Content[:maxLength-3] + "..."
|
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 {
|
func (n *News) WordCount() int {
|
||||||
if n.Content == "" {
|
if n.Content == "" {
|
||||||
return 0
|
return 0
|
||||||
@ -280,22 +262,22 @@ func (n *News) WordCount() int {
|
|||||||
return words
|
return words
|
||||||
}
|
}
|
||||||
|
|
||||||
// Length returns the character length of the content
|
// Returns the character length of the content
|
||||||
func (n *News) Length() int {
|
func (n *News) Length() int {
|
||||||
return len(n.Content)
|
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 {
|
func (n *News) Contains(term string) bool {
|
||||||
return strings.Contains(strings.ToLower(n.Content), strings.ToLower(term))
|
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 {
|
func (n *News) IsEmpty() bool {
|
||||||
return strings.TrimSpace(n.Content) == ""
|
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) {
|
func Search(term string) ([]*News, error) {
|
||||||
var newsPosts []*News
|
var newsPosts []*News
|
||||||
|
|
||||||
@ -314,22 +296,3 @@ func Search(term string) ([]*News, error) {
|
|||||||
|
|
||||||
return newsPosts, nil
|
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 (
|
import (
|
||||||
"dk/internal/auth"
|
"dk/internal/auth"
|
||||||
|
"dk/internal/helpers"
|
||||||
|
"dk/internal/items"
|
||||||
"dk/internal/middleware"
|
"dk/internal/middleware"
|
||||||
"dk/internal/router"
|
"dk/internal/router"
|
||||||
"dk/internal/template/components"
|
"dk/internal/template/components"
|
||||||
@ -17,6 +19,7 @@ func RegisterTownRoutes(r *router.Router) {
|
|||||||
|
|
||||||
group.Get("/", showTown)
|
group.Get("/", showTown)
|
||||||
group.Get("/inn", showInn)
|
group.Get("/inn", showInn)
|
||||||
|
group.Get("/shop", showShop)
|
||||||
group.WithMiddleware(middleware.CSRF(auth.Manager)).Post("/inn", rest)
|
group.WithMiddleware(middleware.CSRF(auth.Manager)).Post("/inn", rest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,3 +61,23 @@ func rest(ctx router.Ctx, _ []string) {
|
|||||||
"rested": true,
|
"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
|
// Spell represents a spell in the database
|
||||||
type Spell struct {
|
type Spell struct {
|
||||||
|
database.BaseModel
|
||||||
|
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
MP int `db:"mp" json:"mp"`
|
MP int `db:"mp" json:"mp"`
|
||||||
@ -18,7 +20,31 @@ type Spell struct {
|
|||||||
Type int `db:"type" json:"type"`
|
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 {
|
func New() *Spell {
|
||||||
return &Spell{
|
return &Spell{
|
||||||
Name: "",
|
Name: "",
|
||||||
@ -30,12 +56,12 @@ func New() *Spell {
|
|||||||
|
|
||||||
var spellScanner = scanner.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 {
|
func spellColumns() string {
|
||||||
return spellScanner.Columns()
|
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 {
|
func scanSpell(stmt *sqlite.Stmt) *Spell {
|
||||||
spell := &Spell{}
|
spell := &Spell{}
|
||||||
spellScanner.Scan(stmt, spell)
|
spellScanner.Scan(stmt, spell)
|
||||||
@ -51,7 +77,7 @@ const (
|
|||||||
TypeDefenseBoost = 5
|
TypeDefenseBoost = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
// Find retrieves a spell by ID
|
// Retrieves a spell by ID
|
||||||
func Find(id int) (*Spell, error) {
|
func Find(id int) (*Spell, error) {
|
||||||
var spell *Spell
|
var spell *Spell
|
||||||
|
|
||||||
@ -73,7 +99,7 @@ func Find(id int) (*Spell, error) {
|
|||||||
return spell, nil
|
return spell, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// All retrieves all spells
|
// Retrieves all spells
|
||||||
func All() ([]*Spell, error) {
|
func All() ([]*Spell, error) {
|
||||||
var spells []*Spell
|
var spells []*Spell
|
||||||
|
|
||||||
@ -92,7 +118,7 @@ func All() ([]*Spell, error) {
|
|||||||
return spells, nil
|
return spells, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByType retrieves spells by type
|
// Retrieves spells by type
|
||||||
func ByType(spellType int) ([]*Spell, error) {
|
func ByType(spellType int) ([]*Spell, error) {
|
||||||
var spells []*Spell
|
var spells []*Spell
|
||||||
|
|
||||||
@ -111,7 +137,7 @@ func ByType(spellType int) ([]*Spell, error) {
|
|||||||
return spells, nil
|
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) {
|
func ByMaxMP(maxMP int) ([]*Spell, error) {
|
||||||
var spells []*Spell
|
var spells []*Spell
|
||||||
|
|
||||||
@ -130,7 +156,7 @@ func ByMaxMP(maxMP int) ([]*Spell, error) {
|
|||||||
return spells, nil
|
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) {
|
func ByTypeAndMaxMP(spellType, maxMP int) ([]*Spell, error) {
|
||||||
var spells []*Spell
|
var spells []*Spell
|
||||||
|
|
||||||
@ -149,7 +175,7 @@ func ByTypeAndMaxMP(spellType, maxMP int) ([]*Spell, error) {
|
|||||||
return spells, nil
|
return spells, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByName retrieves a spell by name (case-insensitive)
|
// Retrieves a spell by name (case-insensitive)
|
||||||
func ByName(name string) (*Spell, error) {
|
func ByName(name string) (*Spell, error) {
|
||||||
var spell *Spell
|
var spell *Spell
|
||||||
|
|
||||||
@ -171,83 +197,39 @@ func ByName(name string) (*Spell, error) {
|
|||||||
return spell, nil
|
return spell, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updates an existing spell in the database
|
// Saves a new spell to the database and sets the ID
|
||||||
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
|
|
||||||
func (s *Spell) Insert() error {
|
func (s *Spell) Insert() error {
|
||||||
if s.ID != 0 {
|
columns := `name, mp, attribute, type`
|
||||||
return fmt.Errorf("spell already has ID %d, use Save() to update", s.ID)
|
values := []any{s.Name, s.MP, s.Attribute, s.Type}
|
||||||
}
|
return database.Insert(s, columns, values...)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the spell from the database
|
// Returns true if the spell is a healing spell
|
||||||
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
|
|
||||||
func (s *Spell) IsHealing() bool {
|
func (s *Spell) IsHealing() bool {
|
||||||
return s.Type == TypeHealing
|
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 {
|
func (s *Spell) IsHurt() bool {
|
||||||
return s.Type == TypeHurt
|
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 {
|
func (s *Spell) IsSleep() bool {
|
||||||
return s.Type == TypeSleep
|
return s.Type == TypeSleep
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAttackBoost returns true if the spell boosts attack
|
// Returns true if the spell boosts attack
|
||||||
func (s *Spell) IsAttackBoost() bool {
|
func (s *Spell) IsAttackBoost() bool {
|
||||||
return s.Type == TypeAttackBoost
|
return s.Type == TypeAttackBoost
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDefenseBoost returns true if the spell boosts defense
|
// Returns true if the spell boosts defense
|
||||||
func (s *Spell) IsDefenseBoost() bool {
|
func (s *Spell) IsDefenseBoost() bool {
|
||||||
return s.Type == TypeDefenseBoost
|
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 {
|
func (s *Spell) TypeName() string {
|
||||||
switch s.Type {
|
switch s.Type {
|
||||||
case TypeHealing:
|
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 {
|
func (s *Spell) CanCast(availableMP int) bool {
|
||||||
return availableMP >= s.MP
|
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 {
|
func (s *Spell) Efficiency() float64 {
|
||||||
if s.MP == 0 {
|
if s.MP == 0 {
|
||||||
return 0
|
return 0
|
||||||
@ -278,34 +260,12 @@ func (s *Spell) Efficiency() float64 {
|
|||||||
return float64(s.Attribute) / float64(s.MP)
|
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 {
|
func (s *Spell) IsOffensive() bool {
|
||||||
return s.Type == TypeHurt || s.Type == TypeSleep
|
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 {
|
func (s *Spell) IsSupport() bool {
|
||||||
return s.Type == TypeHealing || s.Type == TypeAttackBoost || s.Type == TypeDefenseBoost
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"slices"
|
||||||
|
|
||||||
"dk/internal/database"
|
"dk/internal/database"
|
||||||
|
"dk/internal/helpers"
|
||||||
"dk/internal/helpers/scanner"
|
"dk/internal/helpers/scanner"
|
||||||
|
|
||||||
"zombiezen.com/go/sqlite"
|
"zombiezen.com/go/sqlite"
|
||||||
@ -13,6 +14,8 @@ import (
|
|||||||
|
|
||||||
// Town represents a town in the database
|
// Town represents a town in the database
|
||||||
type Town struct {
|
type Town struct {
|
||||||
|
database.BaseModel
|
||||||
|
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
X int `db:"x" json:"x"`
|
X int `db:"x" json:"x"`
|
||||||
@ -23,11 +26,35 @@ type Town struct {
|
|||||||
ShopList string `db:"shop_list" json:"shop_list"`
|
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 {
|
func New() *Town {
|
||||||
return &Town{
|
return &Town{
|
||||||
Name: "",
|
Name: "",
|
||||||
X: 0, // Default coordinates
|
X: 0, // Default coordinates
|
||||||
Y: 0,
|
Y: 0,
|
||||||
InnCost: 50, // Default inn cost
|
InnCost: 50, // Default inn cost
|
||||||
MapCost: 100, // Default map cost
|
MapCost: 100, // Default map cost
|
||||||
@ -38,19 +65,19 @@ func New() *Town {
|
|||||||
|
|
||||||
var townScanner = scanner.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 {
|
func townColumns() string {
|
||||||
return townScanner.Columns()
|
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 {
|
func scanTown(stmt *sqlite.Stmt) *Town {
|
||||||
town := &Town{}
|
town := &Town{}
|
||||||
townScanner.Scan(stmt, town)
|
townScanner.Scan(stmt, town)
|
||||||
return town
|
return town
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find retrieves a town by ID
|
// Retrieves a town by ID
|
||||||
func Find(id int) (*Town, error) {
|
func Find(id int) (*Town, error) {
|
||||||
var town *Town
|
var town *Town
|
||||||
|
|
||||||
@ -72,7 +99,7 @@ func Find(id int) (*Town, error) {
|
|||||||
return town, nil
|
return town, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// All retrieves all towns
|
// Retrieves all towns
|
||||||
func All() ([]*Town, error) {
|
func All() ([]*Town, error) {
|
||||||
var towns []*Town
|
var towns []*Town
|
||||||
|
|
||||||
@ -91,7 +118,7 @@ func All() ([]*Town, error) {
|
|||||||
return towns, nil
|
return towns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByName retrieves a town by name (case-insensitive)
|
// Retrieves a town by name (case-insensitive)
|
||||||
func ByName(name string) (*Town, error) {
|
func ByName(name string) (*Town, error) {
|
||||||
var town *Town
|
var town *Town
|
||||||
|
|
||||||
@ -113,7 +140,7 @@ func ByName(name string) (*Town, error) {
|
|||||||
return town, nil
|
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) {
|
func ByMaxInnCost(maxCost int) ([]*Town, error) {
|
||||||
var towns []*Town
|
var towns []*Town
|
||||||
|
|
||||||
@ -132,7 +159,7 @@ func ByMaxInnCost(maxCost int) ([]*Town, error) {
|
|||||||
return towns, nil
|
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) {
|
func ByMaxTPCost(maxCost int) ([]*Town, error) {
|
||||||
var towns []*Town
|
var towns []*Town
|
||||||
|
|
||||||
@ -151,7 +178,7 @@ func ByMaxTPCost(maxCost int) ([]*Town, error) {
|
|||||||
return towns, nil
|
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) {
|
func ByCoords(x, y int) (*Town, error) {
|
||||||
var town *Town
|
var town *Town
|
||||||
|
|
||||||
@ -169,7 +196,7 @@ func ByCoords(x, y int) (*Town, error) {
|
|||||||
return town, nil
|
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) {
|
func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
|
||||||
var towns []*Town
|
var towns []*Town
|
||||||
|
|
||||||
@ -192,145 +219,72 @@ func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
|
|||||||
return towns, nil
|
return towns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updates an existing town in the database
|
// Saves a new town to the database and sets the ID
|
||||||
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
|
|
||||||
func (t *Town) Insert() error {
|
func (t *Town) Insert() error {
|
||||||
if t.ID != 0 {
|
columns := `name, x, y, inn_cost, map_cost, tp_cost, shop_list`
|
||||||
return fmt.Errorf("town already has ID %d, use Save() to update", t.ID)
|
values := []any{t.Name, t.X, t.Y, t.InnCost, t.MapCost, t.TPCost, t.ShopList}
|
||||||
}
|
return database.Insert(t, columns, values...)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the town from the database
|
// Returns the shop items as a slice of item IDs
|
||||||
func (t *Town) Delete() error {
|
func (t *Town) GetShopItems() []int {
|
||||||
if t.ID == 0 {
|
return helpers.StringToInts(t.ShopList)
|
||||||
return fmt.Errorf("cannot delete town without ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
query := "DELETE FROM towns WHERE id = ?"
|
|
||||||
return database.Exec(query, t.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetShopItems returns the shop items as a slice of item IDs
|
// Sets the shop items from a slice of item IDs
|
||||||
func (t *Town) GetShopItems() []string {
|
func (t *Town) SetShopItems(items []int) {
|
||||||
if t.ShopList == "" {
|
t.Set("ShopList", helpers.IntsToString(items))
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
return strings.Split(t.ShopList, ",")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetShopItems sets the shop items from a slice of item IDs
|
// Checks if the town's shop sells a specific item ID
|
||||||
func (t *Town) SetShopItems(items []string) {
|
func (t *Town) HasShopItem(itemID int) bool {
|
||||||
t.ShopList = strings.Join(items, ",")
|
return slices.Contains(t.GetShopItems(), itemID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasShopItem checks if the town's shop sells a specific item ID
|
// Calculates the squared distance from this town to given coordinates
|
||||||
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
|
|
||||||
func (t *Town) DistanceFromSquared(x, y int) float64 {
|
func (t *Town) DistanceFromSquared(x, y int) float64 {
|
||||||
dx := float64(t.X - x)
|
dx := float64(t.X - x)
|
||||||
dy := float64(t.Y - y)
|
dy := float64(t.Y - y)
|
||||||
return dx*dx + dy*dy // Return squared distance for performance
|
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 {
|
func (t *Town) DistanceFrom(x, y int) float64 {
|
||||||
return math.Sqrt(t.DistanceFromSquared(x, y))
|
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 {
|
func (t *Town) IsStartingTown() bool {
|
||||||
return t.X == 0 && t.Y == 0
|
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 {
|
func (t *Town) CanAffordInn(gold int) bool {
|
||||||
return gold >= t.InnCost
|
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 {
|
func (t *Town) CanAffordMap(gold int) bool {
|
||||||
return gold >= t.MapCost
|
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 {
|
func (t *Town) CanAffordTeleport(gold int) bool {
|
||||||
return gold >= t.TPCost
|
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 {
|
func (t *Town) HasShop() bool {
|
||||||
return len(t.GetShopItems()) > 0
|
return len(t.GetShopItems()) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPosition returns the town's coordinates
|
// Returns the town's coordinates
|
||||||
func (t *Town) GetPosition() (int, int) {
|
func (t *Town) GetPosition() (int, int) {
|
||||||
return t.X, t.Y
|
return t.X, t.Y
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPosition sets the town's coordinates
|
// Sets the town's coordinates
|
||||||
func (t *Town) SetPosition(x, y int) {
|
func (t *Town) SetPosition(x, y int) {
|
||||||
t.X = x
|
t.Set("X", x)
|
||||||
t.Y = y
|
t.Set("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},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -339,71 +339,3 @@ func (u *User) SetPosition(x, y int) {
|
|||||||
u.Set("X", x)
|
u.Set("X", x)
|
||||||
u.Set("Y", y)
|
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>
|
<div class="title"><img src="/assets/images/button_towns.gif" alt="Towns" title="Towns"></div>
|
||||||
{if #_towns > 0}
|
{if #_towns > 0}
|
||||||
<i>Teleport to:</i>
|
<i>Teleport to:</i>
|
||||||
{for map in _towns}
|
{for id,name in _towns}
|
||||||
<a href="#">{map.name}</a>
|
{if town != nil and name == town.Name}
|
||||||
|
<span>{name} <i>(here)</i></span>
|
||||||
|
{else}
|
||||||
|
<a href="#">{name}</a>
|
||||||
|
{/if}
|
||||||
{/for}
|
{/for}
|
||||||
{else}
|
{else}
|
||||||
<i>No town maps</i>
|
<i>No town maps</i>
|
||||||
|
@ -65,8 +65,8 @@
|
|||||||
<section>
|
<section>
|
||||||
<div class="title"><img src="/assets/images/button_fastspells.gif" alt="Fast Spells" title="Fast Spells"></div>
|
<div class="title"><img src="/assets/images/button_fastspells.gif" alt="Fast Spells" title="Fast Spells"></div>
|
||||||
{if #_spells > 0}
|
{if #_spells > 0}
|
||||||
{for spell in _spells}
|
{for id,name in _spells}
|
||||||
<a href="#">{spell.name}</a>
|
<a href="#">{name}</a>
|
||||||
{/for}
|
{/for}
|
||||||
{else}
|
{else}
|
||||||
<i>No known healing spells</i>
|
<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