462 lines
13 KiB
Go
462 lines
13 KiB
Go
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
|
|
} |