package towns import ( "dk/internal/store" "fmt" "math" "slices" "sort" "strconv" "strings" "sync" "dk/internal/helpers" ) // Town represents a town in the game 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"` } func (t *Town) Save() error { townStore := GetStore() townStore.UpdateTown(t) return nil } func (t *Town) Delete() error { townStore := GetStore() townStore.RemoveTown(t.ID) return nil } // 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 } } // Validate checks if town has valid values func (t *Town) Validate() error { if t.Name == "" { return fmt.Errorf("town name cannot be empty") } if t.InnCost < 0 { return fmt.Errorf("town InnCost cannot be negative") } if t.MapCost < 0 { return fmt.Errorf("town MapCost cannot be negative") } if t.TPCost < 0 { return fmt.Errorf("town TPCost cannot be negative") } return nil } // TownStore provides in-memory storage with O(1) lookups and town-specific indices type TownStore struct { *store.BaseStore[Town] // Embedded generic store byName map[string]int // Name (lowercase) -> ID byCoords map[string]int // "x,y" -> ID byInnCost map[int][]int // InnCost -> []ID byTPCost map[int][]int // TPCost -> []ID allByID []int // All IDs sorted by ID mu sync.RWMutex // Protects indices } // Global in-memory store var townStore *TownStore var storeOnce sync.Once // Initialize the in-memory store func initStore() { townStore = &TownStore{ BaseStore: store.NewBaseStore[Town](), byName: make(map[string]int), byCoords: make(map[string]int), byInnCost: make(map[int][]int), byTPCost: make(map[int][]int), allByID: make([]int, 0), } } // GetStore returns the global town store func GetStore() *TownStore { storeOnce.Do(initStore) return townStore } // AddTown adds a town to the in-memory store and updates all indices func (ts *TownStore) AddTown(town *Town) { ts.mu.Lock() defer ts.mu.Unlock() // Validate town if err := town.Validate(); err != nil { return } // Add to base store ts.Add(town.ID, town) // Rebuild indices ts.rebuildIndicesUnsafe() } // RemoveTown removes a town from the store and updates indices func (ts *TownStore) RemoveTown(id int) { ts.mu.Lock() defer ts.mu.Unlock() // Remove from base store ts.Remove(id) // Rebuild indices ts.rebuildIndicesUnsafe() } // UpdateTown updates a town efficiently func (ts *TownStore) UpdateTown(town *Town) { ts.mu.Lock() defer ts.mu.Unlock() // Validate town if err := town.Validate(); err != nil { return } // Update base store ts.Add(town.ID, town) // Rebuild indices ts.rebuildIndicesUnsafe() } // LoadData loads town data from JSON file, or starts with empty store func LoadData(dataPath string) error { ts := GetStore() // Load from base store, which handles JSON loading if err := ts.BaseStore.LoadData(dataPath); err != nil { return err } // Rebuild indices from loaded data ts.rebuildIndices() return nil } // SaveData saves town data to JSON file func SaveData(dataPath string) error { ts := GetStore() return ts.BaseStore.SaveData(dataPath) } // coordsKey creates a key for coordinate-based lookup func coordsKey(x, y int) string { return strconv.Itoa(x) + "," + strconv.Itoa(y) } // rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) func (ts *TownStore) rebuildIndicesUnsafe() { // Clear indices ts.byName = make(map[string]int) ts.byCoords = make(map[string]int) ts.byInnCost = make(map[int][]int) ts.byTPCost = make(map[int][]int) ts.allByID = make([]int, 0) // Collect all towns and build indices allTowns := ts.GetAll() for id, town := range allTowns { // Name index (case-insensitive) ts.byName[strings.ToLower(town.Name)] = id // Coordinates index ts.byCoords[coordsKey(town.X, town.Y)] = id // Cost indices ts.byInnCost[town.InnCost] = append(ts.byInnCost[town.InnCost], id) ts.byTPCost[town.TPCost] = append(ts.byTPCost[town.TPCost], id) // All IDs ts.allByID = append(ts.allByID, id) } // Sort all by ID sort.Ints(ts.allByID) // Sort cost indices by ID for innCost := range ts.byInnCost { sort.Ints(ts.byInnCost[innCost]) } for tpCost := range ts.byTPCost { sort.Ints(ts.byTPCost[tpCost]) } } // rebuildIndices rebuilds all town-specific indices from base store data func (ts *TownStore) rebuildIndices() { ts.mu.Lock() defer ts.mu.Unlock() ts.rebuildIndicesUnsafe() } // Retrieves a town by ID func Find(id int) (*Town, error) { ts := GetStore() town, exists := ts.GetByID(id) if !exists { return nil, fmt.Errorf("town with ID %d not found", id) } return town, nil } // Retrieves all towns func All() ([]*Town, error) { ts := GetStore() ts.mu.RLock() defer ts.mu.RUnlock() result := make([]*Town, 0, len(ts.allByID)) for _, id := range ts.allByID { if town, exists := ts.GetByID(id); exists { result = append(result, town) } } return result, nil } // Retrieves a town by name (case-insensitive) func ByName(name string) (*Town, error) { ts := GetStore() ts.mu.RLock() defer ts.mu.RUnlock() id, exists := ts.byName[strings.ToLower(name)] if !exists { return nil, fmt.Errorf("town with name '%s' not found", name) } town, exists := ts.GetByID(id) if !exists { 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) { ts := GetStore() ts.mu.RLock() defer ts.mu.RUnlock() var result []*Town for cost := 0; cost <= maxCost; cost++ { if ids, exists := ts.byInnCost[cost]; exists { for _, id := range ids { if town, exists := ts.GetByID(id); exists { result = append(result, town) } } } } return result, nil } // Retrieves towns with teleport cost at most the specified amount func ByMaxTPCost(maxCost int) ([]*Town, error) { ts := GetStore() ts.mu.RLock() defer ts.mu.RUnlock() var result []*Town for cost := 0; cost <= maxCost; cost++ { if ids, exists := ts.byTPCost[cost]; exists { for _, id := range ids { if town, exists := ts.GetByID(id); exists { result = append(result, town) } } } } return result, nil } // Retrieves a town by its x, y coordinates func ByCoords(x, y int) (*Town, error) { ts := GetStore() ts.mu.RLock() defer ts.mu.RUnlock() id, exists := ts.byCoords[coordsKey(x, y)] if !exists { return nil, nil // Return nil if not found (like original) } town, exists := ts.GetByID(id) if !exists { return nil, nil } return town, nil } // ExistsAt checks for a town at the given coordinates, returning true/false func ExistsAt(x, y int) bool { ts := GetStore() ts.mu.RLock() defer ts.mu.RUnlock() _, exists := ts.byCoords[coordsKey(x, y)] return exists } // Retrieves towns within a certain distance from a point func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) { ts := GetStore() ts.mu.RLock() defer ts.mu.RUnlock() var result []*Town maxDistance2 := float64(maxDistance * maxDistance) for _, id := range ts.allByID { if town, exists := ts.GetByID(id); exists { if town.DistanceFromSquared(fromX, fromY) <= maxDistance2 { result = append(result, town) } } } // Sort by distance, then by ID sort.Slice(result, func(i, j int) bool { distI := result[i].DistanceFromSquared(fromX, fromY) distJ := result[j].DistanceFromSquared(fromX, fromY) if distI == distJ { return result[i].ID < result[j].ID } return distI < distJ }) return result, nil } // Saves a new town to the in-memory store and sets the ID func (t *Town) Insert() error { ts := GetStore() // Validate before insertion if err := t.Validate(); err != nil { return fmt.Errorf("validation failed: %w", err) } // Assign new ID if not set if t.ID == 0 { t.ID = ts.GetNextID() } // Add to store ts.AddTown(t) return nil } // 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.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.X = x t.Y = y }