eq2go/internal/titles/player_titles.go

462 lines
13 KiB
Go

package titles
import (
"fmt"
"sync"
)
// 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
}