package titles import ( "fmt" "sync" "time" ) // PlayerTitlesList manages titles owned by a specific player type PlayerTitlesList struct { playerID int32 // Character ID titles map[int32]*PlayerTitle // Owned titles indexed by title ID activePrefixID int32 // Currently active prefix title ID (0 = none) activeSuffixID int32 // Currently active suffix title ID (0 = none) masterList *MasterTitlesList // Reference to master titles list mutex sync.RWMutex // Thread safety } // TitlePacketData represents title data for network packets type TitlePacketData struct { PlayerID uint32 `json:"player_id"` PlayerName string `json:"player_name"` PrefixTitle string `json:"prefix_title"` SuffixTitle string `json:"suffix_title"` SubTitle string `json:"sub_title"` LastName string `json:"last_name"` Titles []TitleEntry `json:"titles"` NumTitles uint16 `json:"num_titles"` CurrentPrefix int16 `json:"current_prefix"` CurrentSuffix int16 `json:"current_suffix"` } // TitleEntry represents a single title for packet transmission type TitleEntry struct { Name string `json:"name"` IsPrefix bool `json:"is_prefix"` } // NewPlayerTitlesList creates a new player titles list func NewPlayerTitlesList(playerID int32, masterList *MasterTitlesList) *PlayerTitlesList { ptl := &PlayerTitlesList{ playerID: playerID, titles: make(map[int32]*PlayerTitle), activePrefixID: TitleIDNone, activeSuffixID: TitleIDCitizen, // Default suffix title masterList: masterList, } // Grant default citizen title ptl.grantDefaultTitle() return ptl } // grantDefaultTitle grants the basic citizen title to new players func (ptl *PlayerTitlesList) grantDefaultTitle() { citizenTitle := NewPlayerTitle(TitleIDCitizen, ptl.playerID) citizenTitle.IsActive = true citizenTitle.IsPrefix = false // Suffix title ptl.titles[TitleIDCitizen] = citizenTitle } // AddTitle grants a title to the player func (ptl *PlayerTitlesList) AddTitle(titleID int32, sourceAchievementID, sourceQuestID uint32) error { ptl.mutex.Lock() defer ptl.mutex.Unlock() // Check if player already has this title if _, exists := ptl.titles[titleID]; exists { return fmt.Errorf("player %d already has title %d", ptl.playerID, titleID) } // Verify title exists in master list masterTitle, exists := ptl.masterList.GetTitle(titleID) if !exists { return fmt.Errorf("title %d does not exist in master list", titleID) } // Check if we've hit the maximum title limit if len(ptl.titles) >= MaxPlayerTitles { return fmt.Errorf("player %d has reached maximum title limit of %d", ptl.playerID, MaxPlayerTitles) } // Check for unique title restrictions if masterTitle.IsUnique() { // TODO: Check database to ensure no other player has this unique title } // Create player title entry playerTitle := NewPlayerTitle(titleID, ptl.playerID) playerTitle.AchievementID = sourceAchievementID playerTitle.QuestID = sourceQuestID // Set expiration if it's a temporary title if masterTitle.ExpirationHours > 0 { playerTitle.SetExpiration(masterTitle.ExpirationHours) } ptl.titles[titleID] = playerTitle return nil } // RemoveTitle removes a title from the player func (ptl *PlayerTitlesList) RemoveTitle(titleID int32) error { ptl.mutex.Lock() defer ptl.mutex.Unlock() playerTitle, exists := ptl.titles[titleID] if !exists { return fmt.Errorf("player %d does not have title %d", ptl.playerID, titleID) } // If this title is currently active, deactivate it if playerTitle.IsActive { if playerTitle.IsPrefix && ptl.activePrefixID == titleID { ptl.activePrefixID = TitleIDNone } else if !playerTitle.IsPrefix && ptl.activeSuffixID == titleID { ptl.activeSuffixID = TitleIDCitizen // Revert to default } } delete(ptl.titles, titleID) return nil } // HasTitle checks if the player owns a specific title func (ptl *PlayerTitlesList) HasTitle(titleID int32) bool { ptl.mutex.RLock() defer ptl.mutex.RUnlock() _, exists := ptl.titles[titleID] return exists } // GetTitle retrieves a player's title information func (ptl *PlayerTitlesList) GetTitle(titleID int32) (*PlayerTitle, bool) { ptl.mutex.RLock() defer ptl.mutex.RUnlock() title, exists := ptl.titles[titleID] if !exists { return nil, false } return title.Clone(), true } // SetActivePrefix sets the active prefix title func (ptl *PlayerTitlesList) SetActivePrefix(titleID int32) error { ptl.mutex.Lock() defer ptl.mutex.Unlock() // Allow clearing prefix title if titleID == TitleIDNone { // Deactivate current prefix if any if ptl.activePrefixID != TitleIDNone { if currentTitle, exists := ptl.titles[ptl.activePrefixID]; exists { currentTitle.IsActive = false } } ptl.activePrefixID = TitleIDNone return nil } // Check if player owns the title playerTitle, exists := ptl.titles[titleID] if !exists { return fmt.Errorf("player %d does not own title %d", ptl.playerID, titleID) } // Verify title can be used as prefix masterTitle, exists := ptl.masterList.GetTitle(titleID) if !exists { return fmt.Errorf("title %d not found in master list", titleID) } if masterTitle.Position != TitlePositionPrefix { return fmt.Errorf("title %d cannot be used as prefix", titleID) } // Check if title has expired if playerTitle.IsExpired() { return fmt.Errorf("title %d has expired", titleID) } // Deactivate current prefix if ptl.activePrefixID != TitleIDNone { if currentTitle, exists := ptl.titles[ptl.activePrefixID]; exists { currentTitle.IsActive = false } } // Activate new prefix playerTitle.Activate(true) ptl.activePrefixID = titleID return nil } // SetActiveSuffix sets the active suffix title func (ptl *PlayerTitlesList) SetActiveSuffix(titleID int32) error { ptl.mutex.Lock() defer ptl.mutex.Unlock() // Check if player owns the title playerTitle, exists := ptl.titles[titleID] if !exists { return fmt.Errorf("player %d does not own title %d", ptl.playerID, titleID) } // Verify title can be used as suffix masterTitle, exists := ptl.masterList.GetTitle(titleID) if !exists { return fmt.Errorf("title %d not found in master list", titleID) } if masterTitle.Position != TitlePositionSuffix { return fmt.Errorf("title %d cannot be used as suffix", titleID) } // Check if title has expired if playerTitle.IsExpired() { return fmt.Errorf("title %d has expired", titleID) } // Deactivate current suffix if ptl.activeSuffixID != TitleIDNone { if currentTitle, exists := ptl.titles[ptl.activeSuffixID]; exists { currentTitle.IsActive = false } } // Activate new suffix playerTitle.Activate(false) ptl.activeSuffixID = titleID return nil } // GetActivePrefixTitle returns the currently active prefix title func (ptl *PlayerTitlesList) GetActivePrefixTitle() (*Title, bool) { ptl.mutex.RLock() defer ptl.mutex.RUnlock() if ptl.activePrefixID == TitleIDNone { return nil, false } return ptl.masterList.GetTitle(ptl.activePrefixID) } // GetActiveSuffixTitle returns the currently active suffix title func (ptl *PlayerTitlesList) GetActiveSuffixTitle() (*Title, bool) { ptl.mutex.RLock() defer ptl.mutex.RUnlock() if ptl.activeSuffixID == TitleIDNone { return nil, false } return ptl.masterList.GetTitle(ptl.activeSuffixID) } // GetAllTitles returns all titles owned by the player func (ptl *PlayerTitlesList) GetAllTitles() []*PlayerTitle { ptl.mutex.RLock() defer ptl.mutex.RUnlock() result := make([]*PlayerTitle, 0, len(ptl.titles)) for _, title := range ptl.titles { result = append(result, title.Clone()) } return result } // GetAvailablePrefixTitles returns all titles that can be used as prefix func (ptl *PlayerTitlesList) GetAvailablePrefixTitles() []*Title { ptl.mutex.RLock() defer ptl.mutex.RUnlock() result := make([]*Title, 0) for titleID := range ptl.titles { if masterTitle, exists := ptl.masterList.GetTitle(titleID); exists { if masterTitle.Position == TitlePositionPrefix { // Check if not expired if playerTitle := ptl.titles[titleID]; !playerTitle.IsExpired() { result = append(result, masterTitle) } } } } return result } // GetAvailableSuffixTitles returns all titles that can be used as suffix func (ptl *PlayerTitlesList) GetAvailableSuffixTitles() []*Title { ptl.mutex.RLock() defer ptl.mutex.RUnlock() result := make([]*Title, 0) for titleID := range ptl.titles { if masterTitle, exists := ptl.masterList.GetTitle(titleID); exists { if masterTitle.Position == TitlePositionSuffix { // Check if not expired if playerTitle := ptl.titles[titleID]; !playerTitle.IsExpired() { result = append(result, masterTitle) } } } } return result } // CleanupExpiredTitles removes expired temporary titles func (ptl *PlayerTitlesList) CleanupExpiredTitles() int { ptl.mutex.Lock() defer ptl.mutex.Unlock() expiredCount := 0 expiredTitles := make([]int32, 0) // Find expired titles for titleID, playerTitle := range ptl.titles { if playerTitle.IsExpired() { expiredTitles = append(expiredTitles, titleID) expiredCount++ } } // Remove expired titles for _, titleID := range expiredTitles { playerTitle := ptl.titles[titleID] // If this expired title is currently active, deactivate it if playerTitle.IsActive { if playerTitle.IsPrefix && ptl.activePrefixID == titleID { ptl.activePrefixID = TitleIDNone } else if !playerTitle.IsPrefix && ptl.activeSuffixID == titleID { ptl.activeSuffixID = TitleIDCitizen // Revert to default } } delete(ptl.titles, titleID) } return expiredCount } // GetTitleCount returns the number of titles owned by the player func (ptl *PlayerTitlesList) GetTitleCount() int { ptl.mutex.RLock() defer ptl.mutex.RUnlock() return len(ptl.titles) } // BuildPacketData creates title data for network transmission func (ptl *PlayerTitlesList) BuildPacketData(playerName string) *TitlePacketData { ptl.mutex.RLock() defer ptl.mutex.RUnlock() data := &TitlePacketData{ PlayerID: uint32(ptl.playerID), PlayerName: playerName, PrefixTitle: "", SuffixTitle: "", SubTitle: "", // TODO: Implement subtitle system LastName: "", // TODO: Implement last name system Titles: make([]TitleEntry, 0, len(ptl.titles)), CurrentPrefix: int16(ptl.activePrefixID), CurrentSuffix: int16(ptl.activeSuffixID), } // Get active prefix title name if prefixTitle, exists := ptl.GetActivePrefixTitle(); exists { data.PrefixTitle = prefixTitle.GetDisplayName() } // Get active suffix title name if suffixTitle, exists := ptl.GetActiveSuffixTitle(); exists { data.SuffixTitle = suffixTitle.GetDisplayName() } // Build title array for UI for titleID := range ptl.titles { if masterTitle, exists := ptl.masterList.GetTitle(titleID); exists { // Skip hidden titles unless it's a GM viewing // TODO: Add GM check parameter if masterTitle.IsHidden() { continue } // Skip expired titles if ptl.titles[titleID].IsExpired() { continue } entry := TitleEntry{ Name: masterTitle.GetDisplayName(), IsPrefix: masterTitle.Position == TitlePositionPrefix, } data.Titles = append(data.Titles, entry) } } data.NumTitles = uint16(len(data.Titles)) return data } // GrantTitleFromAchievement grants a title when an achievement is completed func (ptl *PlayerTitlesList) GrantTitleFromAchievement(achievementID uint32) error { // Find title associated with this achievement title, exists := ptl.masterList.GetTitleByAchievement(achievementID) if !exists { return nil // No title associated with this achievement } // Grant the title return ptl.AddTitle(title.ID, achievementID, 0) } // LoadFromDatabase would load player titles from the database // TODO: Implement database integration with zone/database package func (ptl *PlayerTitlesList) LoadFromDatabase() error { // TODO: Implement database loading return fmt.Errorf("LoadFromDatabase not yet implemented - requires database integration") } // SaveToDatabase would save player titles to the database // TODO: Implement database integration with zone/database package func (ptl *PlayerTitlesList) SaveToDatabase() error { // TODO: Implement database saving return fmt.Errorf("SaveToDatabase not yet implemented - requires database integration") } // GetFormattedName returns the player name with active titles applied func (ptl *PlayerTitlesList) GetFormattedName(playerName string) string { ptl.mutex.RLock() defer ptl.mutex.RUnlock() result := playerName // Add prefix if active if prefixTitle, exists := ptl.GetActivePrefixTitle(); exists { result = prefixTitle.GetDisplayName() + " " + result } // Add suffix if active if suffixTitle, exists := ptl.GetActiveSuffixTitle(); exists { result = result + " " + suffixTitle.GetDisplayName() } return result }