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 } // 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) }