377 lines
9.0 KiB
Go
377 lines
9.0 KiB
Go
package monsters
|
|
|
|
import (
|
|
"dk/internal/store"
|
|
"fmt"
|
|
"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 {
|
|
monsterStore := GetStore()
|
|
monsterStore.UpdateMonster(m)
|
|
return nil
|
|
}
|
|
|
|
func (m *Monster) Delete() error {
|
|
monsterStore := GetStore()
|
|
monsterStore.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
|
|
}
|
|
}
|
|
|
|
// Validate checks if monster has valid values
|
|
func (m *Monster) Validate() error {
|
|
if m.Name == "" {
|
|
return fmt.Errorf("monster name cannot be empty")
|
|
}
|
|
if m.MaxHP < 1 {
|
|
return fmt.Errorf("monster MaxHP must be at least 1")
|
|
}
|
|
if m.Level < 1 {
|
|
return fmt.Errorf("monster Level must be at least 1")
|
|
}
|
|
if m.Immune < ImmuneNone || m.Immune > ImmuneSleep {
|
|
return fmt.Errorf("invalid immunity type: %d", m.Immune)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 and monster-specific indices
|
|
type MonsterStore struct {
|
|
*store.BaseStore[Monster] // Embedded generic store
|
|
byLevel map[int][]int // Level -> []ID
|
|
byImmunity map[int][]int // Immunity -> []ID
|
|
allByLevel []int // All IDs sorted by level, then ID
|
|
mu sync.RWMutex // Protects indices
|
|
}
|
|
|
|
// Global in-memory store
|
|
var monsterStore *MonsterStore
|
|
var storeOnce sync.Once
|
|
|
|
// Initialize the in-memory store
|
|
func initStore() {
|
|
monsterStore = &MonsterStore{
|
|
BaseStore: store.NewBaseStore[Monster](),
|
|
byLevel: make(map[int][]int),
|
|
byImmunity: make(map[int][]int),
|
|
allByLevel: make([]int, 0),
|
|
}
|
|
}
|
|
|
|
// GetStore returns the global monster store
|
|
func GetStore() *MonsterStore {
|
|
storeOnce.Do(initStore)
|
|
return monsterStore
|
|
}
|
|
|
|
// 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()
|
|
|
|
// Validate monster
|
|
if err := monster.Validate(); err != nil {
|
|
return
|
|
}
|
|
|
|
// Add to base store
|
|
ms.Add(monster.ID, monster)
|
|
|
|
// Rebuild indices
|
|
ms.rebuildIndicesUnsafe()
|
|
}
|
|
|
|
// RemoveMonster removes a monster from the store and updates indices
|
|
func (ms *MonsterStore) RemoveMonster(id int) {
|
|
ms.mu.Lock()
|
|
defer ms.mu.Unlock()
|
|
|
|
// Remove from base store
|
|
ms.Remove(id)
|
|
|
|
// Rebuild indices
|
|
ms.rebuildIndicesUnsafe()
|
|
}
|
|
|
|
// UpdateMonster updates a monster efficiently
|
|
func (ms *MonsterStore) UpdateMonster(monster *Monster) {
|
|
ms.mu.Lock()
|
|
defer ms.mu.Unlock()
|
|
|
|
// Validate monster
|
|
if err := monster.Validate(); err != nil {
|
|
return
|
|
}
|
|
|
|
// Update base store
|
|
ms.Add(monster.ID, monster)
|
|
|
|
// Rebuild indices
|
|
ms.rebuildIndicesUnsafe()
|
|
}
|
|
|
|
// LoadData loads monster data from JSON file, or starts with empty store
|
|
func LoadData(dataPath string) error {
|
|
ms := GetStore()
|
|
|
|
// Load from base store, which handles JSON loading
|
|
if err := ms.BaseStore.LoadData(dataPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Rebuild indices from loaded data
|
|
ms.rebuildIndices()
|
|
return nil
|
|
}
|
|
|
|
// SaveData saves monster data to JSON file
|
|
func SaveData(dataPath string) error {
|
|
ms := GetStore()
|
|
return ms.BaseStore.SaveData(dataPath)
|
|
}
|
|
|
|
// rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock)
|
|
func (ms *MonsterStore) rebuildIndicesUnsafe() {
|
|
// Clear indices
|
|
ms.byLevel = make(map[int][]int)
|
|
ms.byImmunity = make(map[int][]int)
|
|
ms.allByLevel = make([]int, 0)
|
|
|
|
// Collect all monsters and build indices
|
|
allMonsters := ms.GetAll()
|
|
|
|
// Build level and immunity indices
|
|
for id, monster := range allMonsters {
|
|
ms.byLevel[monster.Level] = append(ms.byLevel[monster.Level], id)
|
|
ms.byImmunity[monster.Immune] = append(ms.byImmunity[monster.Immune], id)
|
|
ms.allByLevel = append(ms.allByLevel, id)
|
|
}
|
|
|
|
// Sort allByLevel by level first, then by ID
|
|
sort.Slice(ms.allByLevel, func(i, j int) bool {
|
|
monsterI, _ := ms.GetByID(ms.allByLevel[i])
|
|
monsterJ, _ := ms.GetByID(ms.allByLevel[j])
|
|
if monsterI.Level == monsterJ.Level {
|
|
return ms.allByLevel[i] < ms.allByLevel[j]
|
|
}
|
|
return monsterI.Level < monsterJ.Level
|
|
})
|
|
|
|
// Sort level indices by ID
|
|
for level := range ms.byLevel {
|
|
sort.Ints(ms.byLevel[level])
|
|
}
|
|
|
|
// Sort immunity indices by level, then ID
|
|
for immunity := range ms.byImmunity {
|
|
sort.Slice(ms.byImmunity[immunity], func(i, j int) bool {
|
|
monsterI, _ := ms.GetByID(ms.byImmunity[immunity][i])
|
|
monsterJ, _ := ms.GetByID(ms.byImmunity[immunity][j])
|
|
if monsterI.Level == monsterJ.Level {
|
|
return ms.byImmunity[immunity][i] < ms.byImmunity[immunity][j]
|
|
}
|
|
return monsterI.Level < monsterJ.Level
|
|
})
|
|
}
|
|
}
|
|
|
|
// rebuildIndices rebuilds all monster-specific indices from base store data
|
|
func (ms *MonsterStore) rebuildIndices() {
|
|
ms.mu.Lock()
|
|
defer ms.mu.Unlock()
|
|
ms.rebuildIndicesUnsafe()
|
|
}
|
|
|
|
// Retrieves a monster by ID - O(1) lookup
|
|
func Find(id int) (*Monster, error) {
|
|
ms := GetStore()
|
|
monster, exists := ms.GetByID(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) {
|
|
ms := GetStore()
|
|
ms.mu.RLock()
|
|
defer ms.mu.RUnlock()
|
|
|
|
result := make([]*Monster, 0, len(ms.allByLevel))
|
|
for _, id := range ms.allByLevel {
|
|
if monster, exists := ms.GetByID(id); exists {
|
|
result = append(result, monster)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Retrieves monsters by level - O(1) lookup
|
|
func ByLevel(level int) ([]*Monster, error) {
|
|
ms := GetStore()
|
|
ms.mu.RLock()
|
|
defer ms.mu.RUnlock()
|
|
|
|
ids, exists := ms.byLevel[level]
|
|
if !exists {
|
|
return []*Monster{}, nil
|
|
}
|
|
|
|
result := make([]*Monster, 0, len(ids))
|
|
for _, id := range ids {
|
|
if monster, exists := ms.GetByID(id); exists {
|
|
result = append(result, monster)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Retrieves monsters within a level range (inclusive) - O(k) where k is result size
|
|
func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) {
|
|
ms := GetStore()
|
|
ms.mu.RLock()
|
|
defer ms.mu.RUnlock()
|
|
|
|
var result []*Monster
|
|
for level := minLevel; level <= maxLevel; level++ {
|
|
if ids, exists := ms.byLevel[level]; exists {
|
|
for _, id := range ids {
|
|
if monster, exists := ms.GetByID(id); exists {
|
|
result = append(result, monster)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Retrieves monsters by immunity type - O(1) lookup
|
|
func ByImmunity(immunityType int) ([]*Monster, error) {
|
|
ms := GetStore()
|
|
ms.mu.RLock()
|
|
defer ms.mu.RUnlock()
|
|
|
|
ids, exists := ms.byImmunity[immunityType]
|
|
if !exists {
|
|
return []*Monster{}, nil
|
|
}
|
|
|
|
result := make([]*Monster, 0, len(ids))
|
|
for _, id := range ids {
|
|
if monster, exists := ms.GetByID(id); exists {
|
|
result = append(result, monster)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Saves a new monster to the in-memory store and sets the ID
|
|
func (m *Monster) Insert() error {
|
|
ms := GetStore()
|
|
|
|
// Validate before insertion
|
|
if err := m.Validate(); err != nil {
|
|
return fmt.Errorf("validation failed: %w", err)
|
|
}
|
|
|
|
// Assign new ID if not set
|
|
if m.ID == 0 {
|
|
m.ID = ms.GetNextID()
|
|
}
|
|
|
|
// Add to store
|
|
ms.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)
|
|
}
|