443 lines
9.9 KiB
Go

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
}