305 lines
7.1 KiB
Go

package towns
import (
"fmt"
"math"
"slices"
"dk/internal/database"
"dk/internal/helpers"
"dk/internal/helpers/scanner"
"zombiezen.com/go/sqlite"
)
// Town represents a town in the database
type Town struct {
database.BaseModel
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"`
X int `db:"x" json:"x"`
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"`
}
func (t *Town) GetTableName() string {
return "towns"
}
func (t *Town) GetID() int {
return t.ID
}
func (t *Town) SetID(id int) {
t.ID = id
}
func (t *Town) Set(field string, value any) error {
return database.Set(t, field, value)
}
func (t *Town) Save() error {
return database.Save(t)
}
func (t *Town) Delete() error {
return database.Delete(t)
}
// Creates a new Town with sensible defaults
func New() *Town {
return &Town{
Name: "",
X: 0, // Default coordinates
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]()
// Returns the column list for town queries
func townColumns() string {
return townScanner.Columns()
}
// Populates a Town struct using the fast scanner
func scanTown(stmt *sqlite.Stmt) *Town {
town := &Town{}
townScanner.Scan(stmt, town)
return town
}
// 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
}
// 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
}
// 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
}
// 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
}
// 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
}
// 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
}
// ExistsAt checks for a town at the given coordinates, returning true/false
func ExistsAt(x, y int) bool {
var exists bool
query := `SELECT COUNT(*) > 0 FROM towns WHERE x = ? AND y = ? LIMIT 1`
err := database.Query(query, func(stmt *sqlite.Stmt) error {
exists = stmt.ColumnInt(0) > 0
return nil
}, x, y)
return err == nil && exists
}
// 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
}
// Saves a new town to the database and sets the ID
func (t *Town) Insert() error {
columns := `name, x, y, inn_cost, map_cost, tp_cost, shop_list`
values := []any{t.Name, t.X, t.Y, t.InnCost, t.MapCost, t.TPCost, t.ShopList}
return database.Insert(t, columns, values...)
}
// Returns the shop items as a slice of item IDs
func (t *Town) GetShopItems() []int {
return helpers.StringToInts(t.ShopList)
}
// Sets the shop items from a slice of item IDs
func (t *Town) SetShopItems(items []int) {
t.Set("ShopList", helpers.IntsToString(items))
}
// Checks if the town's shop sells a specific item ID
func (t *Town) HasShopItem(itemID int) bool {
return slices.Contains(t.GetShopItems(), itemID)
}
// 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
}
// 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))
}
// Returns true if this is the starting town (Midworld)
func (t *Town) IsStartingTown() bool {
return t.X == 0 && t.Y == 0
}
// Returns true if the player can afford the inn
func (t *Town) CanAffordInn(gold int) bool {
return gold >= t.InnCost
}
// Returns true if the player can afford to buy the map
func (t *Town) CanAffordMap(gold int) bool {
return gold >= t.MapCost
}
// Returns true if the player can afford to teleport here
func (t *Town) CanAffordTeleport(gold int) bool {
return gold >= t.TPCost
}
// Returns true if the town has a shop with items
func (t *Town) HasShop() bool {
return len(t.GetShopItems()) > 0
}
// Returns the town's coordinates
func (t *Town) GetPosition() (int, int) {
return t.X, t.Y
}
// Sets the town's coordinates
func (t *Town) SetPosition(x, y int) {
t.Set("X", x)
t.Set("Y", y)
}