package monsters import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "sync" ) // Monster represents a monster in the game type Monster struct { ID int `json:"id"` Name string `json:"name"` MaxHP int `json:"max_hp"` MaxDmg int `json:"max_dmg"` Armor int `json:"armor"` Level int `json:"level"` MaxExp int `json:"max_exp"` MaxGold int `json:"max_gold"` Immune int `json:"immune"` } func (m *Monster) Save() error { store := GetStore() store.UpdateMonster(m) return nil } func (m *Monster) Delete() error { store := GetStore() store.RemoveMonster(m.ID) return nil } // Creates a new Monster with sensible defaults func New() *Monster { return &Monster{ Name: "", MaxHP: 10, // Default HP MaxDmg: 5, // Default damage Armor: 0, // Default armor Level: 1, // Default level MaxExp: 10, // Default exp reward MaxGold: 5, // Default gold reward Immune: ImmuneNone, // No immunity by default } } // Immunity constants for monster immunity types const ( ImmuneNone = 0 ImmuneHurt = 1 // Immune to Hurt spells ImmuneSleep = 2 // Immune to Sleep spells ) // MonsterStore provides in-memory storage with O(1) lookups type MonsterStore struct { monsters map[int]*Monster // ID -> Monster (O(1)) byLevel map[int][]*Monster // Level -> []*Monster (O(1) to get slice) byImmunity map[int][]*Monster // Immunity -> []*Monster (O(1) to get slice) allByLevel []*Monster // Pre-sorted by level, id maxID int mu sync.RWMutex } // Global in-memory store var store *MonsterStore var storeOnce sync.Once // Initialize the in-memory store func initStore() { store = &MonsterStore{ monsters: make(map[int]*Monster), byLevel: make(map[int][]*Monster), byImmunity: make(map[int][]*Monster), allByLevel: make([]*Monster, 0), maxID: 0, } } // GetStore returns the global monster store func GetStore() *MonsterStore { storeOnce.Do(initStore) return store } // AddMonster adds a monster to the in-memory store and updates all indices func (ms *MonsterStore) AddMonster(monster *Monster) { ms.mu.Lock() defer ms.mu.Unlock() // Add to primary store ms.monsters[monster.ID] = monster // Update max ID if monster.ID > ms.maxID { ms.maxID = monster.ID } // Add to level index ms.byLevel[monster.Level] = append(ms.byLevel[monster.Level], monster) // Add to immunity index ms.byImmunity[monster.Immune] = append(ms.byImmunity[monster.Immune], monster) // Add to sorted list and re-sort ms.allByLevel = append(ms.allByLevel, monster) sort.Slice(ms.allByLevel, func(i, j int) bool { if ms.allByLevel[i].Level == ms.allByLevel[j].Level { return ms.allByLevel[i].ID < ms.allByLevel[j].ID } return ms.allByLevel[i].Level < ms.allByLevel[j].Level }) // Sort level index sort.Slice(ms.byLevel[monster.Level], func(i, j int) bool { return ms.byLevel[monster.Level][i].ID < ms.byLevel[monster.Level][j].ID }) // Sort immunity index sort.Slice(ms.byImmunity[monster.Immune], func(i, j int) bool { if ms.byImmunity[monster.Immune][i].Level == ms.byImmunity[monster.Immune][j].Level { return ms.byImmunity[monster.Immune][i].ID < ms.byImmunity[monster.Immune][j].ID } return ms.byImmunity[monster.Immune][i].Level < ms.byImmunity[monster.Immune][j].Level }) } // RemoveMonster removes a monster from the store and updates indices func (ms *MonsterStore) RemoveMonster(id int) { ms.mu.Lock() defer ms.mu.Unlock() monster, exists := ms.monsters[id] if !exists { return } // Remove from primary store delete(ms.monsters, id) // Remove from level index levelMonsters := ms.byLevel[monster.Level] for i, m := range levelMonsters { if m.ID == id { ms.byLevel[monster.Level] = append(levelMonsters[:i], levelMonsters[i+1:]...) break } } // Remove from immunity index immunityMonsters := ms.byImmunity[monster.Immune] for i, m := range immunityMonsters { if m.ID == id { ms.byImmunity[monster.Immune] = append(immunityMonsters[:i], immunityMonsters[i+1:]...) break } } // Remove from sorted list for i, m := range ms.allByLevel { if m.ID == id { ms.allByLevel = append(ms.allByLevel[:i], ms.allByLevel[i+1:]...) break } } } // UpdateMonster updates a monster and rebuilds indices func (ms *MonsterStore) UpdateMonster(monster *Monster) { ms.RemoveMonster(monster.ID) ms.AddMonster(monster) } // GetNextID returns the next available ID func (ms *MonsterStore) GetNextID() int { ms.mu.RLock() defer ms.mu.RUnlock() return ms.maxID + 1 } // LoadFromJSON loads monster data from a JSON file func (ms *MonsterStore) LoadFromJSON(filename string) error { ms.mu.Lock() defer ms.mu.Unlock() data, err := os.ReadFile(filename) if err != nil { if os.IsNotExist(err) { return nil // File doesn't exist, start with empty store } return fmt.Errorf("failed to read monsters JSON: %w", err) } // Handle empty file if len(data) == 0 { return nil // Empty file, start with empty store } var monsters []*Monster if err := json.Unmarshal(data, &monsters); err != nil { return fmt.Errorf("failed to unmarshal monsters JSON: %w", err) } // Clear existing data ms.monsters = make(map[int]*Monster) ms.byLevel = make(map[int][]*Monster) ms.byImmunity = make(map[int][]*Monster) ms.allByLevel = make([]*Monster, 0) ms.maxID = 0 // Add all monsters for _, monster := range monsters { ms.monsters[monster.ID] = monster if monster.ID > ms.maxID { ms.maxID = monster.ID } ms.byLevel[monster.Level] = append(ms.byLevel[monster.Level], monster) ms.byImmunity[monster.Immune] = append(ms.byImmunity[monster.Immune], monster) ms.allByLevel = append(ms.allByLevel, monster) } // Sort all indices sort.Slice(ms.allByLevel, func(i, j int) bool { if ms.allByLevel[i].Level == ms.allByLevel[j].Level { return ms.allByLevel[i].ID < ms.allByLevel[j].ID } return ms.allByLevel[i].Level < ms.allByLevel[j].Level }) for level := range ms.byLevel { sort.Slice(ms.byLevel[level], func(i, j int) bool { return ms.byLevel[level][i].ID < ms.byLevel[level][j].ID }) } for immunity := range ms.byImmunity { sort.Slice(ms.byImmunity[immunity], func(i, j int) bool { if ms.byImmunity[immunity][i].Level == ms.byImmunity[immunity][j].Level { return ms.byImmunity[immunity][i].ID < ms.byImmunity[immunity][j].ID } return ms.byImmunity[immunity][i].Level < ms.byImmunity[immunity][j].Level }) } return nil } // SaveToJSON saves monster data to a JSON file func (ms *MonsterStore) SaveToJSON(filename string) error { ms.mu.RLock() defer ms.mu.RUnlock() monsters := make([]*Monster, 0, len(ms.monsters)) for _, monster := range ms.monsters { monsters = append(monsters, monster) } // Sort by ID for consistent output sort.Slice(monsters, func(i, j int) bool { return monsters[i].ID < monsters[j].ID }) data, err := json.MarshalIndent(monsters, "", " ") if err != nil { return fmt.Errorf("failed to marshal monsters to JSON: %w", err) } if err := os.WriteFile(filename, data, 0644); err != nil { return fmt.Errorf("failed to write monsters JSON: %w", err) } return nil } // findMonstersDataPath finds the monsters.json file relative to the current working directory func findMonstersDataPath() (string, error) { // Try current directory first (cwd/data/monsters.json) if _, err := os.Stat("data/monsters.json"); err == nil { return "data/monsters.json", nil } // Walk up directories to find the data folder dir, err := os.Getwd() if err != nil { return "", err } for { dataPath := filepath.Join(dir, "data", "monsters.json") if _, err := os.Stat(dataPath); err == nil { return dataPath, nil } parent := filepath.Dir(dir) if parent == dir { break // reached root } dir = parent } // Default to current directory if not found return "data/monsters.json", nil } // LoadData loads monster data from JSON file, or initializes with default data func LoadData() error { store := GetStore() dataPath, err := findMonstersDataPath() if err != nil { return fmt.Errorf("failed to find monsters data path: %w", err) } if err := store.LoadFromJSON(dataPath); err != nil { // If JSON doesn't exist, initialize with default monsters if os.IsNotExist(err) { fmt.Println("No existing monster data found, initializing with defaults...") if err := initializeDefaultMonsters(); err != nil { return fmt.Errorf("failed to initialize default monsters: %w", err) } // Save the default data if err := SaveData(); err != nil { return fmt.Errorf("failed to save default monster data: %w", err) } fmt.Printf("Initialized %d default monsters\n", len(store.monsters)) } else { return fmt.Errorf("failed to load from JSON: %w", err) } } else { fmt.Printf("Loaded %d monsters from JSON\n", len(store.monsters)) } return nil } // initializeDefaultMonsters creates the default monster set func initializeDefaultMonsters() error { store := GetStore() // Default monsters from the original SQL data defaultMonsters := []*Monster{ {ID: 1, Name: "Blue Slime", MaxHP: 4, MaxDmg: 3, Armor: 1, Level: 1, MaxExp: 1, MaxGold: 1, Immune: ImmuneNone}, {ID: 2, Name: "Red Slime", MaxHP: 6, MaxDmg: 5, Armor: 1, Level: 1, MaxExp: 2, MaxGold: 1, Immune: ImmuneNone}, {ID: 3, Name: "Critter", MaxHP: 6, MaxDmg: 5, Armor: 2, Level: 1, MaxExp: 4, MaxGold: 2, Immune: ImmuneNone}, {ID: 4, Name: "Creature", MaxHP: 10, MaxDmg: 8, Armor: 2, Level: 2, MaxExp: 4, MaxGold: 2, Immune: ImmuneNone}, {ID: 5, Name: "Shadow", MaxHP: 10, MaxDmg: 9, Armor: 3, Level: 2, MaxExp: 6, MaxGold: 2, Immune: ImmuneHurt}, {ID: 6, Name: "Drake", MaxHP: 11, MaxDmg: 10, Armor: 3, Level: 2, MaxExp: 8, MaxGold: 3, Immune: ImmuneNone}, {ID: 7, Name: "Shade", MaxHP: 12, MaxDmg: 10, Armor: 3, Level: 3, MaxExp: 10, MaxGold: 3, Immune: ImmuneHurt}, {ID: 8, Name: "Drakelor", MaxHP: 14, MaxDmg: 12, Armor: 4, Level: 3, MaxExp: 10, MaxGold: 3, Immune: ImmuneNone}, {ID: 9, Name: "Silver Slime", MaxHP: 15, MaxDmg: 100, Armor: 200, Level: 30, MaxExp: 15, MaxGold: 1000, Immune: ImmuneSleep}, {ID: 10, Name: "Scamp", MaxHP: 16, MaxDmg: 13, Armor: 5, Level: 4, MaxExp: 15, MaxGold: 5, Immune: ImmuneNone}, } for _, monster := range defaultMonsters { store.AddMonster(monster) } return nil } // SaveData saves monster data to JSON file func SaveData() error { store := GetStore() dataPath, err := findMonstersDataPath() if err != nil { return fmt.Errorf("failed to find monsters data path: %w", err) } // Ensure data directory exists dataDir := filepath.Dir(dataPath) if err := os.MkdirAll(dataDir, 0755); err != nil { return fmt.Errorf("failed to create data directory: %w", err) } if err := store.SaveToJSON(dataPath); err != nil { return fmt.Errorf("failed to save monsters to JSON: %w", err) } fmt.Printf("Saved %d monsters to JSON\n", len(store.monsters)) return nil } // Retrieves a monster by ID - O(1) lookup func Find(id int) (*Monster, error) { store := GetStore() store.mu.RLock() defer store.mu.RUnlock() monster, exists := store.monsters[id] if !exists { return nil, fmt.Errorf("monster with ID %d not found", id) } return monster, nil } // Retrieves all monsters - O(1) lookup (returns pre-sorted slice) func All() ([]*Monster, error) { store := GetStore() store.mu.RLock() defer store.mu.RUnlock() // Return a copy of the slice to prevent external modifications result := make([]*Monster, len(store.allByLevel)) copy(result, store.allByLevel) return result, nil } // Retrieves monsters by level - O(1) lookup func ByLevel(level int) ([]*Monster, error) { store := GetStore() store.mu.RLock() defer store.mu.RUnlock() monsters, exists := store.byLevel[level] if !exists { return []*Monster{}, nil } // Return a copy of the slice to prevent external modifications result := make([]*Monster, len(monsters)) copy(result, monsters) return result, nil } // Retrieves monsters within a level range (inclusive) - O(k) where k is result size func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) { store := GetStore() store.mu.RLock() defer store.mu.RUnlock() var result []*Monster for level := minLevel; level <= maxLevel; level++ { if monsters, exists := store.byLevel[level]; exists { result = append(result, monsters...) } } return result, nil } // Retrieves monsters by immunity type - O(1) lookup func ByImmunity(immunityType int) ([]*Monster, error) { store := GetStore() store.mu.RLock() defer store.mu.RUnlock() monsters, exists := store.byImmunity[immunityType] if !exists { return []*Monster{}, nil } // Return a copy of the slice to prevent external modifications result := make([]*Monster, len(monsters)) copy(result, monsters) return result, nil } // Saves a new monster to the in-memory store and sets the ID func (m *Monster) Insert() error { store := GetStore() // Assign new ID if not set if m.ID == 0 { m.ID = store.GetNextID() } // Add to store store.AddMonster(m) return nil } // Returns true if the monster is immune to Hurt spells func (m *Monster) IsHurtImmune() bool { return m.Immune == ImmuneHurt } // Returns true if the monster is immune to Sleep spells func (m *Monster) IsSleepImmune() bool { return m.Immune == ImmuneSleep } // Returns true if the monster has any immunity func (m *Monster) HasImmunity() bool { return m.Immune != ImmuneNone } // Returns the string representation of the monster's immunity func (m *Monster) ImmunityName() string { switch m.Immune { case ImmuneNone: return "None" case ImmuneHurt: return "Hurt Spells" case ImmuneSleep: return "Sleep Spells" default: return "Unknown" } } // Calculates a simple difficulty rating based on stats func (m *Monster) DifficultyRating() float64 { // Simple formula: (HP + Damage + Armor) / Level // Higher values indicate tougher monsters relative to their level if m.Level == 0 { return 0 } return float64(m.MaxHP+m.MaxDmg+m.Armor) / float64(m.Level) } // Returns the experience reward per hit point (efficiency metric) func (m *Monster) ExpPerHP() float64 { if m.MaxHP == 0 { return 0 } return float64(m.MaxExp) / float64(m.MaxHP) } // Returns the gold reward per hit point (efficiency metric) func (m *Monster) GoldPerHP() float64 { if m.MaxHP == 0 { return 0 } return float64(m.MaxGold) / float64(m.MaxHP) }