package towns import ( "fmt" "strings" "dk/internal/database" "zombiezen.com/go/sqlite" ) // Town represents a town in the database type Town struct { ID int `json:"id"` Name string `json:"name"` X int `json:"x"` Y int `json:"y"` InnCost int `json:"inn_cost"` MapCost int `json:"map_cost"` TPCost int `json:"tp_cost"` ShopList string `json:"shop_list"` } // Find retrieves a town by ID func Find(id int) (*Town, error) { town := &Town{} query := "SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list FROM towns WHERE id = ?" err := database.Query(query, func(stmt *sqlite.Stmt) error { town.ID = stmt.ColumnInt(0) town.Name = stmt.ColumnText(1) town.X = stmt.ColumnInt(2) town.Y = stmt.ColumnInt(3) town.InnCost = stmt.ColumnInt(4) town.MapCost = stmt.ColumnInt(5) town.TPCost = stmt.ColumnInt(6) town.ShopList = stmt.ColumnText(7) return nil }, id) if err != nil { return nil, fmt.Errorf("failed to find town: %w", err) } if town.ID == 0 { 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 id, name, x, y, inn_cost, map_cost, tp_cost, shop_list FROM towns ORDER BY id" err := database.Query(query, func(stmt *sqlite.Stmt) error { town := &Town{ ID: stmt.ColumnInt(0), Name: stmt.ColumnText(1), X: stmt.ColumnInt(2), Y: stmt.ColumnInt(3), InnCost: stmt.ColumnInt(4), MapCost: stmt.ColumnInt(5), TPCost: stmt.ColumnInt(6), ShopList: stmt.ColumnText(7), } 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) { town := &Town{} query := "SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list FROM towns WHERE LOWER(name) = LOWER(?) LIMIT 1" err := database.Query(query, func(stmt *sqlite.Stmt) error { town.ID = stmt.ColumnInt(0) town.Name = stmt.ColumnText(1) town.X = stmt.ColumnInt(2) town.Y = stmt.ColumnInt(3) town.InnCost = stmt.ColumnInt(4) town.MapCost = stmt.ColumnInt(5) town.TPCost = stmt.ColumnInt(6) town.ShopList = stmt.ColumnText(7) return nil }, name) if err != nil { return nil, fmt.Errorf("failed to find town by name: %w", err) } if town.ID == 0 { 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 id, name, x, y, inn_cost, map_cost, tp_cost, shop_list FROM towns WHERE inn_cost <= ? ORDER BY inn_cost, id" err := database.Query(query, func(stmt *sqlite.Stmt) error { town := &Town{ ID: stmt.ColumnInt(0), Name: stmt.ColumnText(1), X: stmt.ColumnInt(2), Y: stmt.ColumnInt(3), InnCost: stmt.ColumnInt(4), MapCost: stmt.ColumnInt(5), TPCost: stmt.ColumnInt(6), ShopList: stmt.ColumnText(7), } 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 id, name, x, y, inn_cost, map_cost, tp_cost, shop_list FROM towns WHERE tp_cost <= ? ORDER BY tp_cost, id" err := database.Query(query, func(stmt *sqlite.Stmt) error { town := &Town{ ID: stmt.ColumnInt(0), Name: stmt.ColumnText(1), X: stmt.ColumnInt(2), Y: stmt.ColumnInt(3), InnCost: stmt.ColumnInt(4), MapCost: stmt.ColumnInt(5), TPCost: stmt.ColumnInt(6), ShopList: stmt.ColumnText(7), } 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) { town := &Town{} query := `SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list FROM towns WHERE x = ? AND y = ? LIMIT 1` err := database.Query(query, func(stmt *sqlite.Stmt) error { town.ID = stmt.ColumnInt(0) town.Name = stmt.ColumnText(1) town.X = stmt.ColumnInt(2) town.Y = stmt.ColumnInt(3) town.InnCost = stmt.ColumnInt(4) town.MapCost = stmt.ColumnInt(5) town.TPCost = stmt.ColumnInt(6) town.ShopList = stmt.ColumnText(7) return nil }, x, y) if err != nil { return nil, fmt.Errorf("failed to retrieve towns by distance: %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 id, name, x, y, inn_cost, map_cost, tp_cost, shop_list 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 := &Town{ ID: stmt.ColumnInt(0), Name: stmt.ColumnText(1), X: stmt.ColumnInt(2), Y: stmt.ColumnInt(3), InnCost: stmt.ColumnInt(4), MapCost: stmt.ColumnInt(5), TPCost: stmt.ColumnInt(6), ShopList: stmt.ColumnText(7), } 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) } // 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 distance from this town to given coordinates func (t *Town) DistanceFrom(x, y int) float64 { dx := float64(t.X - x) dy := float64(t.Y - y) return dx*dx + dy*dy // Return squared distance for performance } // 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 }