eq2go/internal/titles/master_list.go
2025-08-06 13:19:07 -05:00

453 lines
12 KiB
Go

package titles
import (
"fmt"
"sync"
)
// MasterTitlesList manages all available titles in the game
type MasterTitlesList struct {
titles map[int32]*Title // All titles indexed by ID
categorized map[string][]*Title // Titles grouped by category
bySource map[int32][]*Title // Titles grouped by source
byRarity map[int32][]*Title // Titles grouped by rarity
byAchievement map[uint32]*Title // Titles indexed by achievement ID
nextID int32 // Next available title ID
mutex sync.RWMutex // Thread safety
}
// NewMasterTitlesList creates a new master titles list
func NewMasterTitlesList() *MasterTitlesList {
mtl := &MasterTitlesList{
titles: make(map[int32]*Title),
categorized: make(map[string][]*Title),
bySource: make(map[int32][]*Title),
byRarity: make(map[int32][]*Title),
byAchievement: make(map[uint32]*Title),
nextID: 1,
}
// Initialize default titles
mtl.initializeDefaultTitles()
return mtl
}
// initializeDefaultTitles creates the basic system titles
func (mtl *MasterTitlesList) initializeDefaultTitles() {
// System titles with negative IDs
citizen := NewTitle(TitleIDCitizen, "Citizen")
citizen.SetDescription("Default citizen title")
citizen.SetFlag(FlagStarter)
citizen.Position = TitlePositionSuffix
mtl.addTitleInternal(citizen)
visitor := NewTitle(TitleIDVisitor, "Visitor")
visitor.SetDescription("Temporary visitor status")
visitor.SetFlag(FlagTemporary)
visitor.Position = TitlePositionSuffix
mtl.addTitleInternal(visitor)
newcomer := NewTitle(TitleIDNewcomer, "Newcomer")
newcomer.SetDescription("New player welcome title")
newcomer.SetFlag(FlagStarter)
newcomer.ExpirationHours = 168 // 1 week
newcomer.Position = TitlePositionPrefix
mtl.addTitleInternal(newcomer)
returning := NewTitle(TitleIDReturning, "Returning")
returning.SetDescription("Welcome back title for returning players")
returning.SetFlag(FlagTemporary)
returning.ExpirationHours = 72 // 3 days
returning.Position = TitlePositionPrefix
mtl.addTitleInternal(returning)
}
// AddTitle adds a new title to the master list
func (mtl *MasterTitlesList) AddTitle(title *Title) error {
mtl.mutex.Lock()
defer mtl.mutex.Unlock()
if title == nil {
return fmt.Errorf("cannot add nil title")
}
// Assign ID if not set
if title.ID == 0 {
title.ID = mtl.nextID
mtl.nextID++
}
// Check for duplicate ID
if _, exists := mtl.titles[title.ID]; exists {
return fmt.Errorf("title with ID %d already exists", title.ID)
}
// Validate title name length
if len(title.Name) > MaxTitleNameLength {
return fmt.Errorf("title name exceeds maximum length of %d characters", MaxTitleNameLength)
}
// Validate description length
if len(title.Description) > MaxTitleDescriptionLength {
return fmt.Errorf("title description exceeds maximum length of %d characters", MaxTitleDescriptionLength)
}
// Check for unique titles
if title.IsUnique() {
// TODO: Check if any player already has this unique title
}
return mtl.addTitleInternal(title)
}
// addTitleInternal adds a title without validation (used internally)
func (mtl *MasterTitlesList) addTitleInternal(title *Title) error {
// Add to main map
mtl.titles[title.ID] = title
// Add to category index
if mtl.categorized[title.Category] == nil {
mtl.categorized[title.Category] = make([]*Title, 0)
}
mtl.categorized[title.Category] = append(mtl.categorized[title.Category], title)
// Add to source index
if mtl.bySource[title.Source] == nil {
mtl.bySource[title.Source] = make([]*Title, 0)
}
mtl.bySource[title.Source] = append(mtl.bySource[title.Source], title)
// Add to rarity index
if mtl.byRarity[title.Rarity] == nil {
mtl.byRarity[title.Rarity] = make([]*Title, 0)
}
mtl.byRarity[title.Rarity] = append(mtl.byRarity[title.Rarity], title)
// Add to achievement index if applicable
if title.AchievementID > 0 {
mtl.byAchievement[title.AchievementID] = title
}
// Update next ID if necessary
if title.ID >= mtl.nextID {
mtl.nextID = title.ID + 1
}
return nil
}
// GetTitle retrieves a title by ID
func (mtl *MasterTitlesList) GetTitle(id int32) (*Title, bool) {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
title, exists := mtl.titles[id]
if !exists {
return nil, false
}
return title.Clone(), true
}
// GetTitleByName retrieves a title by name (case-sensitive)
func (mtl *MasterTitlesList) GetTitleByName(name string) (*Title, bool) {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
for _, title := range mtl.titles {
if title.Name == name {
return title.Clone(), true
}
}
return nil, false
}
// GetTitleByAchievement retrieves a title associated with an achievement
func (mtl *MasterTitlesList) GetTitleByAchievement(achievementID uint32) (*Title, bool) {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
title, exists := mtl.byAchievement[achievementID]
if !exists {
return nil, false
}
return title.Clone(), true
}
// GetTitlesByCategory retrieves all titles in a specific category
func (mtl *MasterTitlesList) GetTitlesByCategory(category string) []*Title {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
titles := mtl.categorized[category]
if titles == nil {
return make([]*Title, 0)
}
// Return clones to prevent external modification
result := make([]*Title, len(titles))
for i, title := range titles {
result[i] = title.Clone()
}
return result
}
// GetTitlesBySource retrieves all titles from a specific source
func (mtl *MasterTitlesList) GetTitlesBySource(source int32) []*Title {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
titles := mtl.bySource[source]
if titles == nil {
return make([]*Title, 0)
}
// Return clones to prevent external modification
result := make([]*Title, len(titles))
for i, title := range titles {
result[i] = title.Clone()
}
return result
}
// GetTitlesByRarity retrieves all titles of a specific rarity
func (mtl *MasterTitlesList) GetTitlesByRarity(rarity int32) []*Title {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
titles := mtl.byRarity[rarity]
if titles == nil {
return make([]*Title, 0)
}
// Return clones to prevent external modification
result := make([]*Title, len(titles))
for i, title := range titles {
result[i] = title.Clone()
}
return result
}
// GetAllTitles retrieves all titles (excluding hidden ones by default)
func (mtl *MasterTitlesList) GetAllTitles(includeHidden bool) []*Title {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
result := make([]*Title, 0, len(mtl.titles))
for _, title := range mtl.titles {
if !includeHidden && title.IsHidden() {
continue
}
result = append(result, title.Clone())
}
return result
}
// GetAvailableCategories returns all categories that have titles
func (mtl *MasterTitlesList) GetAvailableCategories() []string {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
categories := make([]string, 0, len(mtl.categorized))
for category := range mtl.categorized {
categories = append(categories, category)
}
return categories
}
// RemoveTitle removes a title from the master list
func (mtl *MasterTitlesList) RemoveTitle(id int32) error {
mtl.mutex.Lock()
defer mtl.mutex.Unlock()
title, exists := mtl.titles[id]
if !exists {
return fmt.Errorf("title with ID %d does not exist", id)
}
// Remove from main map
delete(mtl.titles, id)
// Remove from category index
categorySlice := mtl.categorized[title.Category]
mtl.removeFromSlice(&categorySlice, title)
mtl.categorized[title.Category] = categorySlice
if len(mtl.categorized[title.Category]) == 0 {
delete(mtl.categorized, title.Category)
}
// Remove from source index
sourceSlice := mtl.bySource[title.Source]
mtl.removeFromSlice(&sourceSlice, title)
mtl.bySource[title.Source] = sourceSlice
if len(mtl.bySource[title.Source]) == 0 {
delete(mtl.bySource, title.Source)
}
// Remove from rarity index
raritySlice := mtl.byRarity[title.Rarity]
mtl.removeFromSlice(&raritySlice, title)
mtl.byRarity[title.Rarity] = raritySlice
if len(mtl.byRarity[title.Rarity]) == 0 {
delete(mtl.byRarity, title.Rarity)
}
// Remove from achievement index if applicable
if title.AchievementID > 0 {
delete(mtl.byAchievement, title.AchievementID)
}
return nil
}
// removeFromSlice removes a title from a slice
func (mtl *MasterTitlesList) removeFromSlice(slice *[]*Title, title *Title) {
for i, t := range *slice {
if t.ID == title.ID {
*slice = append((*slice)[:i], (*slice)[i+1:]...)
break
}
}
}
// UpdateTitle updates an existing title
func (mtl *MasterTitlesList) UpdateTitle(title *Title) error {
mtl.mutex.Lock()
defer mtl.mutex.Unlock()
if title == nil {
return fmt.Errorf("cannot update with nil title")
}
existing, exists := mtl.titles[title.ID]
if !exists {
return fmt.Errorf("title with ID %d does not exist", title.ID)
}
// Remove old title from indices
categorySlice := mtl.categorized[existing.Category]
mtl.removeFromSlice(&categorySlice, existing)
mtl.categorized[existing.Category] = categorySlice
sourceSlice := mtl.bySource[existing.Source]
mtl.removeFromSlice(&sourceSlice, existing)
mtl.bySource[existing.Source] = sourceSlice
raritySlice := mtl.byRarity[existing.Rarity]
mtl.removeFromSlice(&raritySlice, existing)
mtl.byRarity[existing.Rarity] = raritySlice
if existing.AchievementID > 0 {
delete(mtl.byAchievement, existing.AchievementID)
}
// Update the title
mtl.titles[title.ID] = title
// Re-add to indices with new values
if mtl.categorized[title.Category] == nil {
mtl.categorized[title.Category] = make([]*Title, 0)
}
mtl.categorized[title.Category] = append(mtl.categorized[title.Category], title)
if mtl.bySource[title.Source] == nil {
mtl.bySource[title.Source] = make([]*Title, 0)
}
mtl.bySource[title.Source] = append(mtl.bySource[title.Source], title)
if mtl.byRarity[title.Rarity] == nil {
mtl.byRarity[title.Rarity] = make([]*Title, 0)
}
mtl.byRarity[title.Rarity] = append(mtl.byRarity[title.Rarity], title)
if title.AchievementID > 0 {
mtl.byAchievement[title.AchievementID] = title
}
return nil
}
// GetTitleCount returns the total number of titles
func (mtl *MasterTitlesList) GetTitleCount() int {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
return len(mtl.titles)
}
// ValidateTitle checks if a title meets all requirements
func (mtl *MasterTitlesList) ValidateTitle(title *Title) error {
if title == nil {
return fmt.Errorf("title cannot be nil")
}
if len(title.Name) == 0 {
return fmt.Errorf("title name cannot be empty")
}
if len(title.Name) > MaxTitleNameLength {
return fmt.Errorf("title name exceeds maximum length of %d characters", MaxTitleNameLength)
}
if len(title.Description) > MaxTitleDescriptionLength {
return fmt.Errorf("title description exceeds maximum length of %d characters", MaxTitleDescriptionLength)
}
if len(title.Requirements) > MaxTitleRequirements {
return fmt.Errorf("title has too many requirements (max %d)", MaxTitleRequirements)
}
// Validate position
if title.Position != TitlePositionPrefix && title.Position != TitlePositionSuffix {
return fmt.Errorf("invalid title position: %d", title.Position)
}
// Validate rarity
if title.Rarity < TitleRarityCommon || title.Rarity > TitleRarityUnique {
return fmt.Errorf("invalid title rarity: %d", title.Rarity)
}
return nil
}
// LoadFromDatabase loads titles from the database
func (mtl *MasterTitlesList) LoadFromDatabase(db *DB) error {
mtl.mutex.Lock()
defer mtl.mutex.Unlock()
// Load all titles from database
titles, err := db.LoadMasterTitles()
if err != nil {
return fmt.Errorf("failed to load titles from database: %w", err)
}
for _, title := range titles {
mtl.addTitleInternal(title)
}
return nil
}
// SaveToDatabase saves titles to the database
func (mtl *MasterTitlesList) SaveToDatabase(db *DB) error {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
// Convert map to slice
titles := make([]*Title, 0, len(mtl.titles))
for _, title := range mtl.titles {
titles = append(titles, title)
}
return db.SaveMasterTitles(titles)
}