532 lines
14 KiB
Go
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)
|
|
}
|