337 lines
8.3 KiB
Go
337 lines
8.3 KiB
Go
package towns
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"dk/internal/database"
|
|
"dk/internal/helpers/scanner"
|
|
|
|
"zombiezen.com/go/sqlite"
|
|
)
|
|
|
|
// Town represents a town in the database
|
|
type Town struct {
|
|
ID int `db:"id" json:"id"`
|
|
Name string `db:"name" json:"name"`
|
|
X int `db:"x" json:"x"`
|
|
Y int `db:"y" json:"y"`
|
|
InnCost int `db:"inn_cost" json:"inn_cost"`
|
|
MapCost int `db:"map_cost" json:"map_cost"`
|
|
TPCost int `db:"tp_cost" json:"tp_cost"`
|
|
ShopList string `db:"shop_list" json:"shop_list"`
|
|
}
|
|
|
|
// New creates a new Town with sensible defaults
|
|
func New() *Town {
|
|
return &Town{
|
|
Name: "",
|
|
X: 0, // Default coordinates
|
|
Y: 0,
|
|
InnCost: 50, // Default inn cost
|
|
MapCost: 100, // Default map cost
|
|
TPCost: 25, // Default teleport cost
|
|
ShopList: "", // No items by default
|
|
}
|
|
}
|
|
|
|
var townScanner = scanner.New[Town]()
|
|
|
|
// townColumns returns the column list for town queries
|
|
func townColumns() string {
|
|
return townScanner.Columns()
|
|
}
|
|
|
|
// scanTown populates a Town struct using the fast scanner
|
|
func scanTown(stmt *sqlite.Stmt) *Town {
|
|
town := &Town{}
|
|
townScanner.Scan(stmt, town)
|
|
return town
|
|
}
|
|
|
|
// Find retrieves a town by ID
|
|
func Find(id int) (*Town, error) {
|
|
var town *Town
|
|
|
|
query := `SELECT ` + townColumns() + ` FROM towns WHERE id = ?`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
town = scanTown(stmt)
|
|
return nil
|
|
}, id)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find town: %w", err)
|
|
}
|
|
|
|
if town == nil {
|
|
return nil, fmt.Errorf("town with ID %d not found", id)
|
|
}
|
|
|
|
return town, nil
|
|
}
|
|
|
|
// All retrieves all towns
|
|
func All() ([]*Town, error) {
|
|
var towns []*Town
|
|
|
|
query := `SELECT ` + townColumns() + ` FROM towns ORDER BY id`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
town := scanTown(stmt)
|
|
towns = append(towns, town)
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve all towns: %w", err)
|
|
}
|
|
|
|
return towns, nil
|
|
}
|
|
|
|
// ByName retrieves a town by name (case-insensitive)
|
|
func ByName(name string) (*Town, error) {
|
|
var town *Town
|
|
|
|
query := `SELECT ` + townColumns() + ` FROM towns WHERE LOWER(name) = LOWER(?) LIMIT 1`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
town = scanTown(stmt)
|
|
return nil
|
|
}, name)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find town by name: %w", err)
|
|
}
|
|
|
|
if town == nil {
|
|
return nil, fmt.Errorf("town with name '%s' not found", name)
|
|
}
|
|
|
|
return town, nil
|
|
}
|
|
|
|
// ByMaxInnCost retrieves towns with inn cost at most the specified amount
|
|
func ByMaxInnCost(maxCost int) ([]*Town, error) {
|
|
var towns []*Town
|
|
|
|
query := `SELECT ` + townColumns() + ` FROM towns WHERE inn_cost <= ? ORDER BY inn_cost, id`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
town := scanTown(stmt)
|
|
towns = append(towns, town)
|
|
return nil
|
|
}, maxCost)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve towns by max inn cost: %w", err)
|
|
}
|
|
|
|
return towns, nil
|
|
}
|
|
|
|
// ByMaxTPCost retrieves towns with teleport cost at most the specified amount
|
|
func ByMaxTPCost(maxCost int) ([]*Town, error) {
|
|
var towns []*Town
|
|
|
|
query := `SELECT ` + townColumns() + ` FROM towns WHERE tp_cost <= ? ORDER BY tp_cost, id`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
town := scanTown(stmt)
|
|
towns = append(towns, town)
|
|
return nil
|
|
}, maxCost)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve towns by max TP cost: %w", err)
|
|
}
|
|
|
|
return towns, nil
|
|
}
|
|
|
|
// ByCoords retrieves a town by its x, y coordinates
|
|
func ByCoords(x, y int) (*Town, error) {
|
|
var town *Town
|
|
|
|
query := `SELECT ` + townColumns() + ` FROM towns WHERE x = ? AND y = ? LIMIT 1`
|
|
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
town = scanTown(stmt)
|
|
return nil
|
|
}, x, y)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve town by coordinates: %w", err)
|
|
}
|
|
|
|
return town, nil
|
|
}
|
|
|
|
// ByDistance retrieves towns within a certain distance from a point
|
|
func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
|
|
var towns []*Town
|
|
|
|
query := `SELECT ` + townColumns() + `
|
|
FROM towns
|
|
WHERE ((x - ?) * (x - ?) + (y - ?) * (y - ?)) <= ?
|
|
ORDER BY ((x - ?) * (x - ?) + (y - ?) * (y - ?)), id`
|
|
|
|
maxDistance2 := maxDistance * maxDistance
|
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
town := scanTown(stmt)
|
|
towns = append(towns, town)
|
|
return nil
|
|
}, fromX, fromX, fromY, fromY, maxDistance2, fromX, fromX, fromY, fromY)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve towns by distance: %w", err)
|
|
}
|
|
|
|
return towns, nil
|
|
}
|
|
|
|
// Save updates an existing town in the database
|
|
func (t *Town) Save() error {
|
|
if t.ID == 0 {
|
|
return fmt.Errorf("cannot save town without ID")
|
|
}
|
|
|
|
query := `UPDATE towns SET name = ?, x = ?, y = ?, inn_cost = ?, map_cost = ?, tp_cost = ?, shop_list = ? WHERE id = ?`
|
|
return database.Exec(query, t.Name, t.X, t.Y, t.InnCost, t.MapCost, t.TPCost, t.ShopList, t.ID)
|
|
}
|
|
|
|
// Insert saves a new town to the database and sets the ID
|
|
func (t *Town) Insert() error {
|
|
if t.ID != 0 {
|
|
return fmt.Errorf("town already has ID %d, use Save() to update", t.ID)
|
|
}
|
|
|
|
// Use a transaction to ensure we can get the ID
|
|
err := database.Transaction(func(tx *database.Tx) error {
|
|
query := `INSERT INTO towns (name, x, y, inn_cost, map_cost, tp_cost, shop_list) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
|
|
if err := tx.Exec(query, t.Name, t.X, t.Y, t.InnCost, t.MapCost, t.TPCost, t.ShopList); err != nil {
|
|
return fmt.Errorf("failed to insert town: %w", err)
|
|
}
|
|
|
|
// Get the last insert ID
|
|
var id int
|
|
err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error {
|
|
id = stmt.ColumnInt(0)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get insert ID: %w", err)
|
|
}
|
|
|
|
t.ID = id
|
|
return nil
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
// Delete removes the town from the database
|
|
func (t *Town) Delete() error {
|
|
if t.ID == 0 {
|
|
return fmt.Errorf("cannot delete town without ID")
|
|
}
|
|
|
|
query := "DELETE FROM towns WHERE id = ?"
|
|
return database.Exec(query, t.ID)
|
|
}
|
|
|
|
// GetShopItems returns the shop items as a slice of item IDs
|
|
func (t *Town) GetShopItems() []string {
|
|
if t.ShopList == "" {
|
|
return []string{}
|
|
}
|
|
return strings.Split(t.ShopList, ",")
|
|
}
|
|
|
|
// SetShopItems sets the shop items from a slice of item IDs
|
|
func (t *Town) SetShopItems(items []string) {
|
|
t.ShopList = strings.Join(items, ",")
|
|
}
|
|
|
|
// HasShopItem checks if the town's shop sells a specific item ID
|
|
func (t *Town) HasShopItem(itemID string) bool {
|
|
items := t.GetShopItems()
|
|
for _, item := range items {
|
|
if strings.TrimSpace(item) == itemID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// DistanceFrom calculates the squared distance from this town to given coordinates
|
|
func (t *Town) DistanceFromSquared(x, y int) float64 {
|
|
dx := float64(t.X - x)
|
|
dy := float64(t.Y - y)
|
|
return dx*dx + dy*dy // Return squared distance for performance
|
|
}
|
|
|
|
// DistanceFrom calculates the actual distance from this town to given coordinates
|
|
func (t *Town) DistanceFrom(x, y int) float64 {
|
|
return math.Sqrt(t.DistanceFromSquared(x, y))
|
|
}
|
|
|
|
// IsStartingTown returns true if this is the starting town (Midworld)
|
|
func (t *Town) IsStartingTown() bool {
|
|
return t.X == 0 && t.Y == 0
|
|
}
|
|
|
|
// CanAffordInn returns true if the player can afford the inn
|
|
func (t *Town) CanAffordInn(gold int) bool {
|
|
return gold >= t.InnCost
|
|
}
|
|
|
|
// CanAffordMap returns true if the player can afford to buy the map
|
|
func (t *Town) CanAffordMap(gold int) bool {
|
|
return gold >= t.MapCost
|
|
}
|
|
|
|
// CanAffordTeleport returns true if the player can afford to teleport here
|
|
func (t *Town) CanAffordTeleport(gold int) bool {
|
|
return gold >= t.TPCost
|
|
}
|
|
|
|
// HasShop returns true if the town has a shop with items
|
|
func (t *Town) HasShop() bool {
|
|
return len(t.GetShopItems()) > 0
|
|
}
|
|
|
|
// GetPosition returns the town's coordinates
|
|
func (t *Town) GetPosition() (int, int) {
|
|
return t.X, t.Y
|
|
}
|
|
|
|
// SetPosition sets the town's coordinates
|
|
func (t *Town) SetPosition(x, y int) {
|
|
t.X = x
|
|
t.Y = y
|
|
}
|
|
|
|
// ToMap converts the town to a map for efficient template rendering
|
|
func (t *Town) ToMap() map[string]any {
|
|
return map[string]any{
|
|
"ID": t.ID,
|
|
"Name": t.Name,
|
|
"X": t.X,
|
|
"Y": t.Y,
|
|
"InnCost": t.InnCost,
|
|
"MapCost": t.MapCost,
|
|
"TPCost": t.TPCost,
|
|
"ShopList": t.ShopList,
|
|
|
|
// Computed values
|
|
"ShopItems": t.GetShopItems(),
|
|
"HasShop": t.HasShop(),
|
|
"IsStartingTown": t.IsStartingTown(),
|
|
"Position": map[string]int{"X": t.X, "Y": t.Y},
|
|
}
|
|
}
|