532 lines
14 KiB
Go

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