453 lines
12 KiB
Go
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)
|
|
}
|