simplify player
This commit is contained in:
parent
bab7d9abf9
commit
a5444ed0b9
@ -23,6 +23,7 @@ This document outlines how we successfully simplified the EverQuest II housing p
|
||||
- NPC/AI
|
||||
- NPC/Race Types
|
||||
- Object
|
||||
- Player
|
||||
|
||||
## Before: Complex Architecture (8 Files, ~2000+ Lines)
|
||||
|
||||
|
@ -51,6 +51,9 @@ const (
|
||||
OP_UpdateCharacterSheetMsg
|
||||
OP_UpdateSpellBookMsg
|
||||
OP_UpdateInventoryMsg
|
||||
OP_CharacterPet
|
||||
OP_UpdateRaidMsg
|
||||
OP_CharacterCurrency
|
||||
|
||||
// Zone transitions
|
||||
OP_ChangeZoneMsg
|
||||
|
@ -1,142 +0,0 @@
|
||||
package player
|
||||
|
||||
// SetCharacterFlag sets a character flag
|
||||
func (p *Player) SetCharacterFlag(flag int) {
|
||||
if flag > CF_MAXIMUM_FLAG {
|
||||
return
|
||||
}
|
||||
|
||||
if flag < 32 {
|
||||
p.SetPlayerFlags(p.GetPlayerFlags() | (1 << uint(flag)))
|
||||
} else {
|
||||
p.SetPlayerFlags2(p.GetPlayerFlags2() | (1 << uint(flag-32)))
|
||||
}
|
||||
p.SetCharSheetChanged(true)
|
||||
}
|
||||
|
||||
// ResetCharacterFlag resets a character flag
|
||||
func (p *Player) ResetCharacterFlag(flag int) {
|
||||
if flag > CF_MAXIMUM_FLAG {
|
||||
return
|
||||
}
|
||||
|
||||
if flag < 32 {
|
||||
p.SetPlayerFlags(p.GetPlayerFlags() & ^(1 << uint(flag)))
|
||||
} else {
|
||||
p.SetPlayerFlags2(p.GetPlayerFlags2() & ^(1 << uint(flag-32)))
|
||||
}
|
||||
p.SetCharSheetChanged(true)
|
||||
}
|
||||
|
||||
// ToggleCharacterFlag toggles a character flag
|
||||
func (p *Player) ToggleCharacterFlag(flag int) {
|
||||
if flag > CF_MAXIMUM_FLAG {
|
||||
return
|
||||
}
|
||||
|
||||
if p.GetCharacterFlag(flag) {
|
||||
p.ResetCharacterFlag(flag)
|
||||
} else {
|
||||
p.SetCharacterFlag(flag)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCharacterFlag returns whether a character flag is set
|
||||
func (p *Player) GetCharacterFlag(flag int) bool {
|
||||
if flag > CF_MAXIMUM_FLAG {
|
||||
return false
|
||||
}
|
||||
|
||||
var ret bool
|
||||
if flag < 32 {
|
||||
ret = (p.GetPlayerFlags() & (1 << uint(flag))) != 0
|
||||
} else {
|
||||
ret = (p.GetPlayerFlags2() & (1 << uint(flag-32))) != 0
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// ControlFlagsChanged returns whether control flags have changed
|
||||
func (p *Player) ControlFlagsChanged() bool {
|
||||
return p.controlFlags.ControlFlagsChanged()
|
||||
}
|
||||
|
||||
// SetPlayerControlFlag sets a player control flag
|
||||
func (p *Player) SetPlayerControlFlag(param, paramValue int8, isActive bool) {
|
||||
p.controlFlags.SetPlayerControlFlag(param, paramValue, isActive)
|
||||
}
|
||||
|
||||
// SendControlFlagUpdates sends control flag updates to the client
|
||||
func (p *Player) SendControlFlagUpdates(client *Client) {
|
||||
p.controlFlags.SendControlFlagUpdates(client)
|
||||
}
|
||||
|
||||
// NewPlayerControlFlags creates a new PlayerControlFlags instance
|
||||
func NewPlayerControlFlags() PlayerControlFlags {
|
||||
return PlayerControlFlags{
|
||||
flagsChanged: false,
|
||||
flagChanges: make(map[int8]map[int8]int8),
|
||||
currentFlags: make(map[int8]map[int8]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// SetPlayerControlFlag sets a control flag
|
||||
func (pcf *PlayerControlFlags) SetPlayerControlFlag(param, paramValue int8, isActive bool) {
|
||||
pcf.controlMutex.Lock()
|
||||
defer pcf.controlMutex.Unlock()
|
||||
|
||||
if pcf.currentFlags[param] == nil {
|
||||
pcf.currentFlags[param] = make(map[int8]bool)
|
||||
}
|
||||
|
||||
if pcf.currentFlags[param][paramValue] != isActive {
|
||||
pcf.currentFlags[param][paramValue] = isActive
|
||||
|
||||
pcf.changesMutex.Lock()
|
||||
if pcf.flagChanges[param] == nil {
|
||||
pcf.flagChanges[param] = make(map[int8]int8)
|
||||
}
|
||||
if isActive {
|
||||
pcf.flagChanges[param][paramValue] = 1
|
||||
} else {
|
||||
pcf.flagChanges[param][paramValue] = 0
|
||||
}
|
||||
pcf.flagsChanged = true
|
||||
pcf.changesMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// ControlFlagsChanged returns whether flags have changed
|
||||
func (pcf *PlayerControlFlags) ControlFlagsChanged() bool {
|
||||
pcf.changesMutex.Lock()
|
||||
defer pcf.changesMutex.Unlock()
|
||||
return pcf.flagsChanged
|
||||
}
|
||||
|
||||
// SendControlFlagUpdates sends flag updates to client
|
||||
func (pcf *PlayerControlFlags) SendControlFlagUpdates(client *Client) {
|
||||
pcf.changesMutex.Lock()
|
||||
defer pcf.changesMutex.Unlock()
|
||||
|
||||
if !pcf.flagsChanged {
|
||||
return
|
||||
}
|
||||
|
||||
// Send control flag updates to client
|
||||
for category, flags := range pcf.flagChanges {
|
||||
for flagIndex, value := range flags {
|
||||
// TODO: When packet system is available, create and send appropriate packets
|
||||
// packet := CreateControlFlagPacket(category, flagIndex, value)
|
||||
// client.SendPacket(packet)
|
||||
|
||||
// For now, just log the change
|
||||
_ = category
|
||||
_ = flagIndex
|
||||
_ = value
|
||||
}
|
||||
}
|
||||
|
||||
// Clear changes after sending
|
||||
pcf.flagChanges = make(map[int8]map[int8]int8)
|
||||
pcf.flagsChanged = false
|
||||
}
|
@ -1,289 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"eq2emu/internal/entity"
|
||||
)
|
||||
|
||||
// InCombat sets the player's combat state
|
||||
func (p *Player) InCombat(val bool, ranged bool) {
|
||||
if val {
|
||||
// Entering combat
|
||||
if ranged {
|
||||
p.SetCharacterFlag(CF_RANGED_AUTO_ATTACK)
|
||||
p.SetRangeAttack(true)
|
||||
} else {
|
||||
p.SetCharacterFlag(CF_AUTO_ATTACK)
|
||||
}
|
||||
|
||||
// Set combat state
|
||||
prevState := p.GetPlayerEngageCommands()
|
||||
if ranged {
|
||||
p.SetPlayerEngageCommands(prevState | RANGE_COMBAT_STATE)
|
||||
} else {
|
||||
p.SetPlayerEngageCommands(prevState | MELEE_COMBAT_STATE)
|
||||
}
|
||||
} else {
|
||||
// Leaving combat
|
||||
if ranged {
|
||||
p.ResetCharacterFlag(CF_RANGED_AUTO_ATTACK)
|
||||
p.SetRangeAttack(false)
|
||||
prevState := p.GetPlayerEngageCommands()
|
||||
p.SetPlayerEngageCommands(prevState & ^RANGE_COMBAT_STATE)
|
||||
} else {
|
||||
p.ResetCharacterFlag(CF_AUTO_ATTACK)
|
||||
prevState := p.GetPlayerEngageCommands()
|
||||
p.SetPlayerEngageCommands(prevState & ^MELEE_COMBAT_STATE)
|
||||
}
|
||||
|
||||
// Clear combat target if leaving all combat
|
||||
if p.GetPlayerEngageCommands() == 0 {
|
||||
p.combatTarget = nil
|
||||
}
|
||||
}
|
||||
|
||||
p.SetCharSheetChanged(true)
|
||||
}
|
||||
|
||||
// ProcessCombat processes combat actions
|
||||
func (p *Player) ProcessCombat() {
|
||||
// Check if in combat
|
||||
if p.GetPlayerEngageCommands() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have a valid target
|
||||
if p.combatTarget == nil || IsDead(p.combatTarget) {
|
||||
p.StopCombat(0)
|
||||
return
|
||||
}
|
||||
|
||||
// Check distance to target
|
||||
distance := p.GetDistance(p.combatTarget.GetX(), p.combatTarget.GetY(), p.combatTarget.GetZ(), true)
|
||||
|
||||
// Process based on combat type
|
||||
if p.rangeAttack {
|
||||
// Ranged combat
|
||||
maxRange := p.GetRangeWeaponRange()
|
||||
if distance > maxRange {
|
||||
// Too far for ranged
|
||||
// TODO: Send out of range message
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Process ranged auto-attack
|
||||
} else {
|
||||
// Melee combat
|
||||
maxRange := p.GetMeleeWeaponRange()
|
||||
if distance > maxRange {
|
||||
// Too far for melee
|
||||
// TODO: Send out of range message
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Process melee auto-attack
|
||||
}
|
||||
}
|
||||
|
||||
// GetRangeWeaponRange returns the range of the equipped ranged weapon
|
||||
func (p *Player) GetRangeWeaponRange() float32 {
|
||||
// TODO: Get from equipped ranged weapon
|
||||
return 35.0 // Default bow range
|
||||
}
|
||||
|
||||
// GetMeleeWeaponRange returns the range of melee weapons
|
||||
func (p *Player) GetMeleeWeaponRange() float32 {
|
||||
// TODO: Adjust based on weapon type and mob size
|
||||
return 5.0 // Default melee range
|
||||
}
|
||||
|
||||
// SetCombatTarget sets the current combat target
|
||||
func (p *Player) SetCombatTarget(target *entity.Entity) {
|
||||
p.combatTarget = target
|
||||
}
|
||||
|
||||
// GetCombatTarget returns the current combat target
|
||||
func (p *Player) GetCombatTarget() *entity.Entity {
|
||||
return p.combatTarget
|
||||
}
|
||||
|
||||
// DamageEquippedItems damages equipped items by durability
|
||||
func (p *Player) DamageEquippedItems(amount int8, client *Client) bool {
|
||||
// TODO: Implement item durability damage
|
||||
// This would:
|
||||
// 1. Get all equipped items
|
||||
// 2. Reduce durability by amount
|
||||
// 3. Check if any items broke
|
||||
// 4. Send updates to client
|
||||
return false
|
||||
}
|
||||
|
||||
// GetTSArrowColor returns the arrow color for tradeskill con
|
||||
func (p *Player) GetTSArrowColor(level int8) int8 {
|
||||
levelDiff := int(level) - int(p.GetTSLevel())
|
||||
|
||||
if levelDiff >= 10 {
|
||||
return 4 // Red
|
||||
} else if levelDiff >= 5 {
|
||||
return 3 // Orange
|
||||
} else if levelDiff >= 1 {
|
||||
return 2 // Yellow
|
||||
} else if levelDiff >= -5 {
|
||||
return 1 // White
|
||||
} else if levelDiff >= -9 {
|
||||
return 0 // Blue
|
||||
} else {
|
||||
return 6 // Green
|
||||
}
|
||||
}
|
||||
|
||||
// CheckLevelStatus checks and updates level-based statuses
|
||||
func (p *Player) CheckLevelStatus(newLevel int16) bool {
|
||||
// TODO: Implement level status checks
|
||||
// This would check things like:
|
||||
// - Mentoring status
|
||||
// - Level-locked abilities
|
||||
// - Zone level requirements
|
||||
// - etc.
|
||||
return true
|
||||
}
|
||||
|
||||
// CalculatePlayerHPPower calculates HP and Power for the player
|
||||
func (p *Player) CalculatePlayerHPPower(newLevel int16) {
|
||||
if newLevel == 0 {
|
||||
newLevel = int16(p.GetLevel())
|
||||
}
|
||||
|
||||
// TODO: Implement proper HP/Power calculation
|
||||
// This is a simplified version
|
||||
|
||||
// Base HP calculation
|
||||
baseHP := int32(50 + (newLevel * 20))
|
||||
staminaBonus := int32(p.GetInfoStruct().GetSta() * 10)
|
||||
totalHP := baseHP + staminaBonus
|
||||
|
||||
// Base Power calculation
|
||||
basePower := int32(50 + (newLevel * 10))
|
||||
primaryStatBonus := p.GetPrimaryStat() * 10
|
||||
totalPower := basePower + primaryStatBonus
|
||||
|
||||
// Set the values
|
||||
p.SetTotalHP(totalHP)
|
||||
p.SetTotalPower(totalPower)
|
||||
|
||||
// Set current values if needed
|
||||
if p.GetHP() > totalHP {
|
||||
p.SetHP(totalHP)
|
||||
}
|
||||
if p.GetPower() > totalPower {
|
||||
p.SetPower(totalPower)
|
||||
}
|
||||
}
|
||||
|
||||
// IsAllowedCombatEquip checks if combat equipment changes are allowed
|
||||
func (p *Player) IsAllowedCombatEquip(slot int8, sendMessage bool) bool {
|
||||
// Can't change equipment while:
|
||||
// - Dead
|
||||
// - In combat (for certain slots)
|
||||
// - Casting
|
||||
// - Stunned/Mezzed
|
||||
|
||||
if p.IsDead() {
|
||||
if sendMessage {
|
||||
// TODO: Send "You cannot change equipment while dead" message
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if in combat
|
||||
if p.GetPlayerEngageCommands() != 0 {
|
||||
// Some slots can't be changed in combat
|
||||
// TODO: Define which slots are restricted
|
||||
restrictedSlots := []int8{0, 1, 2} // Example: primary, secondary, ranged
|
||||
for _, restrictedSlot := range restrictedSlots {
|
||||
if slot == restrictedSlot || slot == -1 { // -1 = all slots
|
||||
if sendMessage {
|
||||
// TODO: Send "You cannot change that equipment in combat" message
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if casting
|
||||
if p.IsCasting() {
|
||||
if sendMessage {
|
||||
// TODO: Send "You cannot change equipment while casting" message
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check control effects
|
||||
if p.IsStunned() || p.IsMezzed() {
|
||||
if sendMessage {
|
||||
// TODO: Send appropriate message
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCasting returns whether the player is currently casting
|
||||
func (p *Player) IsCasting() bool {
|
||||
// TODO: Check actual casting state
|
||||
return false
|
||||
}
|
||||
|
||||
// DismissAllPets dismisses all of the player's pets
|
||||
func (p *Player) DismissAllPets() {
|
||||
// TODO: Implement pet dismissal
|
||||
// This would:
|
||||
// 1. Get all pets (combat, non-combat, deity, etc.)
|
||||
// 2. Remove them from world
|
||||
// 3. Clear pet references
|
||||
// 4. Send updates to client
|
||||
}
|
||||
|
||||
// MentorTarget mentors the current target
|
||||
func (p *Player) MentorTarget() {
|
||||
target := p.GetTarget()
|
||||
if target == nil {
|
||||
// TODO: Send "Invalid mentor target" message
|
||||
return
|
||||
}
|
||||
|
||||
targetPlayer, ok := target.(*Player)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if target is valid for mentoring
|
||||
if targetPlayer.GetLevel() >= p.GetLevel() {
|
||||
// TODO: Send "Target must be lower level" message
|
||||
return
|
||||
}
|
||||
|
||||
// Set mentor stats
|
||||
p.SetMentorStats(int32(targetPlayer.GetLevel()), targetPlayer.GetCharacterID(), true)
|
||||
}
|
||||
|
||||
// SetMentorStats sets the player's effective level for mentoring
|
||||
func (p *Player) SetMentorStats(effectiveLevel int32, targetCharID int32, updateStats bool) {
|
||||
if effectiveLevel < 1 || effectiveLevel > int32(p.GetLevel()) {
|
||||
effectiveLevel = int32(p.GetLevel())
|
||||
}
|
||||
|
||||
p.GetInfoStruct().SetEffectiveLevel(int16(effectiveLevel))
|
||||
|
||||
if updateStats {
|
||||
// TODO: Recalculate all stats for new effective level
|
||||
p.CalculatePlayerHPPower(int16(effectiveLevel))
|
||||
// TODO: Update other stats (mitigation, avoidance, etc.)
|
||||
}
|
||||
|
||||
if effectiveLevel < int32(p.GetLevel()) {
|
||||
p.EnableResetMentorship()
|
||||
}
|
||||
|
||||
p.SetCharSheetChanged(true)
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
package player
|
||||
|
||||
// AddCoins adds coins to the player
|
||||
func (p *Player) AddCoins(val int64) {
|
||||
p.AddCoin(val)
|
||||
p.sendCurrencyUpdate()
|
||||
}
|
||||
|
||||
// RemoveCoins removes coins from the player
|
||||
func (p *Player) RemoveCoins(val int64) bool {
|
||||
if p.GetCoin() >= val {
|
||||
p.SubtractCoin(val)
|
||||
p.sendCurrencyUpdate()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasCoins checks if the player has enough coins
|
||||
func (p *Player) HasCoins(val int64) bool {
|
||||
return p.GetCoin() >= val
|
||||
}
|
||||
|
||||
// GetCoinsCopper returns the copper coin amount
|
||||
func (p *Player) GetCoinsCopper() int32 {
|
||||
return p.GetInfoStructCoinCopper()
|
||||
}
|
||||
|
||||
// GetCoinsSilver returns the silver coin amount
|
||||
func (p *Player) GetCoinsSilver() int32 {
|
||||
return p.GetInfoStructCoinSilver()
|
||||
}
|
||||
|
||||
// GetCoinsGold returns the gold coin amount
|
||||
func (p *Player) GetCoinsGold() int32 {
|
||||
return p.GetInfoStructCoinGold()
|
||||
}
|
||||
|
||||
// GetCoinsPlat returns the platinum coin amount
|
||||
func (p *Player) GetCoinsPlat() int32 {
|
||||
return p.GetInfoStructCoinPlat()
|
||||
}
|
||||
|
||||
// GetBankCoinsCopper returns the bank copper coin amount
|
||||
func (p *Player) GetBankCoinsCopper() int32 {
|
||||
return p.GetInfoStructBankCoinCopper()
|
||||
}
|
||||
|
||||
// GetBankCoinsSilver returns the bank silver coin amount
|
||||
func (p *Player) GetBankCoinsSilver() int32 {
|
||||
return p.GetInfoStructBankCoinSilver()
|
||||
}
|
||||
|
||||
// GetBankCoinsGold returns the bank gold coin amount
|
||||
func (p *Player) GetBankCoinsGold() int32 {
|
||||
return p.GetInfoStructBankCoinGold()
|
||||
}
|
||||
|
||||
// GetBankCoinsPlat returns the bank platinum coin amount
|
||||
func (p *Player) GetBankCoinsPlat() int32 {
|
||||
return p.GetInfoStructBankCoinPlat()
|
||||
}
|
||||
|
||||
// GetStatusPoints returns the player's status points
|
||||
func (p *Player) GetStatusPoints() int32 {
|
||||
return p.GetInfoStructStatusPoints()
|
||||
}
|
||||
|
||||
// sendCurrencyUpdate sends currency update packet to client
|
||||
func (p *Player) sendCurrencyUpdate() {
|
||||
// TODO: When packet system is available, send currency update packet
|
||||
// packet := CreateCurrencyUpdatePacket(p.GetInfoStruct())
|
||||
// p.GetClient().SendPacket(packet)
|
||||
|
||||
// For now, mark that currency has changed
|
||||
if p.GetInfoStruct() != nil {
|
||||
// Currency update will be sent on next info struct update
|
||||
}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// PlayerDatabase manages player data persistence using MySQL
|
||||
type PlayerDatabase struct {
|
||||
db *database.Database
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewPlayerDatabase creates a new player database instance
|
||||
func NewPlayerDatabase(db *database.Database) *PlayerDatabase {
|
||||
return &PlayerDatabase{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadPlayer loads a player from the database
|
||||
func (pdb *PlayerDatabase) LoadPlayer(characterID int32) (*Player, error) {
|
||||
pdb.mutex.RLock()
|
||||
defer pdb.mutex.RUnlock()
|
||||
|
||||
player := NewPlayer()
|
||||
player.SetCharacterID(characterID)
|
||||
|
||||
query := `SELECT name, level, race, class, zone_id, x, y, z, heading
|
||||
FROM characters WHERE id = ?`
|
||||
|
||||
row := pdb.db.QueryRow(query, characterID)
|
||||
var name string
|
||||
var level int16
|
||||
var race, class int8
|
||||
var zoneID int32
|
||||
var x, y, z, heading float32
|
||||
|
||||
err := row.Scan(&name, &level, &race, &class, &zoneID, &x, &y, &z, &heading)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("player not found: %d", characterID)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to load player: %w", err)
|
||||
}
|
||||
|
||||
player.SetName(name)
|
||||
player.SetLevel(level)
|
||||
player.SetRace(race)
|
||||
player.SetClass(class)
|
||||
player.SetZone(zoneID)
|
||||
player.SetX(x)
|
||||
player.SetY(y, false)
|
||||
player.SetZ(z)
|
||||
player.SetHeadingFromFloat(heading)
|
||||
|
||||
return player, nil
|
||||
}
|
||||
|
||||
// SavePlayer saves a player to the database
|
||||
func (pdb *PlayerDatabase) SavePlayer(player *Player) error {
|
||||
if player == nil {
|
||||
return fmt.Errorf("cannot save nil player")
|
||||
}
|
||||
|
||||
pdb.mutex.Lock()
|
||||
defer pdb.mutex.Unlock()
|
||||
|
||||
characterID := player.GetCharacterID()
|
||||
if characterID == 0 {
|
||||
// Insert new player
|
||||
return pdb.insertPlayer(player)
|
||||
}
|
||||
|
||||
// Try to update existing player first
|
||||
return pdb.updatePlayer(player)
|
||||
}
|
||||
|
||||
// insertPlayer inserts a new player record
|
||||
func (pdb *PlayerDatabase) insertPlayer(player *Player) error {
|
||||
query := `INSERT INTO characters
|
||||
(name, level, race, class, zone_id, x, y, z, heading, created_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`
|
||||
|
||||
result, err := pdb.db.Exec(query,
|
||||
player.GetName(),
|
||||
player.GetLevel(),
|
||||
player.GetRace(),
|
||||
player.GetClass(),
|
||||
player.GetZone(),
|
||||
player.GetX(),
|
||||
player.GetY(),
|
||||
player.GetZ(),
|
||||
player.GetHeading(),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert player: %w", err)
|
||||
}
|
||||
|
||||
// Get the inserted character ID
|
||||
characterID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get inserted character ID: %w", err)
|
||||
}
|
||||
|
||||
player.SetCharacterID(int32(characterID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// updatePlayer updates an existing player record
|
||||
func (pdb *PlayerDatabase) updatePlayer(player *Player) error {
|
||||
query := `UPDATE characters
|
||||
SET name=?, level=?, race=?, class=?, zone_id=?, x=?, y=?, z=?, heading=?
|
||||
WHERE id=?`
|
||||
|
||||
_, err := pdb.db.Exec(query,
|
||||
player.GetName(),
|
||||
player.GetLevel(),
|
||||
player.GetRace(),
|
||||
player.GetClass(),
|
||||
player.GetZone(),
|
||||
player.GetX(),
|
||||
player.GetY(),
|
||||
player.GetZ(),
|
||||
player.GetHeading(),
|
||||
player.GetCharacterID(),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update player: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePlayer soft-deletes a player (marks as deleted)
|
||||
func (pdb *PlayerDatabase) DeletePlayer(characterID int32) error {
|
||||
pdb.mutex.Lock()
|
||||
defer pdb.mutex.Unlock()
|
||||
|
||||
query := `UPDATE characters SET deleted_date = NOW() WHERE id = ?`
|
||||
|
||||
_, err := pdb.db.Exec(query, characterID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete player %d: %w", characterID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PlayerExists checks if a player exists in the database
|
||||
func (pdb *PlayerDatabase) PlayerExists(characterID int32) (bool, error) {
|
||||
pdb.mutex.RLock()
|
||||
defer pdb.mutex.RUnlock()
|
||||
|
||||
var count int
|
||||
query := `SELECT COUNT(*) FROM characters WHERE id = ? AND (deleted_date IS NULL OR deleted_date = 0)`
|
||||
|
||||
err := pdb.db.QueryRow(query, characterID).Scan(&count)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check player existence: %w", err)
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
@ -1,303 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/spawn"
|
||||
)
|
||||
|
||||
// GetXPVitality returns the player's adventure XP vitality
|
||||
func (p *Player) GetXPVitality() float32 {
|
||||
return p.GetInfoStructXPVitality()
|
||||
}
|
||||
|
||||
// GetTSXPVitality returns the player's tradeskill XP vitality
|
||||
func (p *Player) GetTSXPVitality() float32 {
|
||||
return p.GetInfoStructTSXPVitality()
|
||||
}
|
||||
|
||||
// AdventureXPEnabled returns whether adventure XP is enabled
|
||||
func (p *Player) AdventureXPEnabled() bool {
|
||||
return p.GetInfoStructXPDebt() < 95.0 && p.GetCharacterFlag(CF_COMBAT_EXPERIENCE_ENABLED)
|
||||
}
|
||||
|
||||
// TradeskillXPEnabled returns whether tradeskill XP is enabled
|
||||
func (p *Player) TradeskillXPEnabled() bool {
|
||||
return p.GetInfoStructTSXPDebt() < 95.0 && p.GetCharacterFlag(CF_QUEST_EXPERIENCE_ENABLED)
|
||||
}
|
||||
|
||||
// SetNeededXP sets the needed XP to a specific value
|
||||
func (p *Player) SetNeededXP(val int32) {
|
||||
p.SetInfoStructXPNeeded(float64(val))
|
||||
}
|
||||
|
||||
// SetNeededXP sets the needed XP based on current level
|
||||
func (p *Player) SetNeededXPByLevel() {
|
||||
p.SetInfoStructXPNeeded(float64(GetNeededXPByLevel(p.GetLevel())))
|
||||
}
|
||||
|
||||
// SetXP sets the current XP
|
||||
func (p *Player) SetXP(val int32) {
|
||||
p.SetInfoStructXP(float64(val))
|
||||
}
|
||||
|
||||
// SetNeededTSXP sets the needed tradeskill XP to a specific value
|
||||
func (p *Player) SetNeededTSXP(val int32) {
|
||||
p.SetInfoStructTSXPNeeded(float64(val))
|
||||
}
|
||||
|
||||
// SetNeededTSXPByLevel sets the needed tradeskill XP based on current level
|
||||
func (p *Player) SetNeededTSXPByLevel() {
|
||||
p.SetInfoStructTSXPNeeded(float64(GetNeededXPByLevel(p.GetTSLevel())))
|
||||
}
|
||||
|
||||
// SetTSXP sets the current tradeskill XP
|
||||
func (p *Player) SetTSXP(val int32) {
|
||||
p.SetInfoStructTSXP(float64(val))
|
||||
}
|
||||
|
||||
// GetNeededXP returns the XP needed for next level
|
||||
func (p *Player) GetNeededXP() int32 {
|
||||
return int32(p.GetInfoStructXPNeeded())
|
||||
}
|
||||
|
||||
// GetXPDebt returns the current XP debt percentage
|
||||
func (p *Player) GetXPDebt() float32 {
|
||||
return p.GetInfoStructXPDebt()
|
||||
}
|
||||
|
||||
// GetXP returns the current XP
|
||||
func (p *Player) GetXP() int32 {
|
||||
return int32(p.GetInfoStructXP())
|
||||
}
|
||||
|
||||
// GetNeededTSXP returns the tradeskill XP needed for next level
|
||||
func (p *Player) GetNeededTSXP() int32 {
|
||||
return int32(p.GetInfoStructTSXPNeeded())
|
||||
}
|
||||
|
||||
// GetTSXP returns the current tradeskill XP
|
||||
func (p *Player) GetTSXP() int32 {
|
||||
return int32(p.GetInfoStructTSXP())
|
||||
}
|
||||
|
||||
// AddXP adds adventure XP to the player
|
||||
func (p *Player) AddXP(xpAmount int32) bool {
|
||||
if xpAmount <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
currentXP := int32(p.GetInfoStructXP())
|
||||
neededXP := int32(p.GetInfoStructXPNeeded())
|
||||
totalXP := currentXP + xpAmount
|
||||
|
||||
// Check if we've reached next level
|
||||
if totalXP >= neededXP {
|
||||
// Level up!
|
||||
if p.GetLevel() < 100 { // Assuming max level is 100
|
||||
// Calculate overflow XP
|
||||
overflow := totalXP - neededXP
|
||||
|
||||
// Level up
|
||||
p.SetLevel(int16(p.GetLevel())+1)
|
||||
p.SetNeededXPByLevel()
|
||||
|
||||
// Set XP to overflow amount
|
||||
p.SetXP(overflow)
|
||||
|
||||
// TODO: Send level up packet/message
|
||||
// TODO: Update stats for new level
|
||||
// TODO: Check for new abilities/spells
|
||||
|
||||
return true
|
||||
} else {
|
||||
// At max level, just set to max
|
||||
p.SetXP(neededXP - 1)
|
||||
}
|
||||
} else {
|
||||
p.SetXP(totalXP)
|
||||
}
|
||||
|
||||
// TODO: Send XP update packet
|
||||
p.SetCharSheetChanged(true)
|
||||
return false
|
||||
}
|
||||
|
||||
// AddTSXP adds tradeskill XP to the player
|
||||
func (p *Player) AddTSXP(xpAmount int32) bool {
|
||||
if xpAmount <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
currentXP := int32(p.GetInfoStructTSXP())
|
||||
neededXP := int32(p.GetInfoStructTSXPNeeded())
|
||||
totalXP := currentXP + xpAmount
|
||||
|
||||
// Check if we've reached next level
|
||||
if totalXP >= neededXP {
|
||||
// Level up!
|
||||
if p.GetTSLevel() < 100 { // Assuming max TS level is 100
|
||||
// Calculate overflow XP
|
||||
overflow := totalXP - neededXP
|
||||
|
||||
// Level up
|
||||
p.SetTSLevel(p.GetTSLevel() + 1)
|
||||
p.SetNeededTSXPByLevel()
|
||||
|
||||
// Set XP to overflow amount
|
||||
p.SetTSXP(overflow)
|
||||
|
||||
// TODO: Send level up packet/message
|
||||
// TODO: Update stats for new level
|
||||
// TODO: Check for new recipes
|
||||
|
||||
return true
|
||||
} else {
|
||||
// At max level, just set to max
|
||||
p.SetTSXP(neededXP - 1)
|
||||
}
|
||||
} else {
|
||||
p.SetTSXP(totalXP)
|
||||
}
|
||||
|
||||
// TODO: Send XP update packet
|
||||
p.SetCharSheetChanged(true)
|
||||
return true
|
||||
}
|
||||
|
||||
// DoubleXPEnabled returns whether double XP is enabled
|
||||
func (p *Player) DoubleXPEnabled() bool {
|
||||
// TODO: Check for double XP events, potions, etc.
|
||||
return false
|
||||
}
|
||||
|
||||
// CalculateXP calculates the XP reward from a victim
|
||||
func (p *Player) CalculateXP(victim *spawn.Spawn) float32 {
|
||||
if victim == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// TODO: Implement full XP calculation formula
|
||||
// This is a simplified version
|
||||
|
||||
victimLevel := victim.GetLevel()
|
||||
playerLevel := p.GetLevel()
|
||||
levelDiff := int(victimLevel) - int(playerLevel)
|
||||
|
||||
// Base XP value
|
||||
baseXP := float32(100 + (victimLevel * 10))
|
||||
|
||||
// Level difference modifier
|
||||
var levelMod float32 = 1.0
|
||||
if levelDiff < -5 {
|
||||
// Grey con, minimal XP
|
||||
levelMod = 0.1
|
||||
} else if levelDiff < -2 {
|
||||
// Green con, reduced XP
|
||||
levelMod = 0.5
|
||||
} else if levelDiff <= 2 {
|
||||
// Blue/White con, normal XP
|
||||
levelMod = 1.0
|
||||
} else if levelDiff <= 4 {
|
||||
// Yellow con, bonus XP
|
||||
levelMod = 1.2
|
||||
} else {
|
||||
// Orange/Red con, high bonus XP
|
||||
levelMod = 1.5
|
||||
}
|
||||
|
||||
// Group modifier
|
||||
groupMod := float32(1.0)
|
||||
if p.group != nil {
|
||||
// TODO: Calculate group bonus
|
||||
groupMod = 0.8 // Simplified group penalty
|
||||
}
|
||||
|
||||
// Vitality modifier
|
||||
vitalityMod := float32(1.0)
|
||||
if p.GetXPVitality() > 0 {
|
||||
vitalityMod = 2.0 // Double XP with vitality
|
||||
}
|
||||
|
||||
// Double XP modifier
|
||||
doubleXPMod := float32(1.0)
|
||||
if p.DoubleXPEnabled() {
|
||||
doubleXPMod = 2.0
|
||||
}
|
||||
|
||||
totalXP := baseXP * levelMod * groupMod * vitalityMod * doubleXPMod
|
||||
return totalXP
|
||||
}
|
||||
|
||||
// CalculateTSXP calculates tradeskill XP for a given level
|
||||
func (p *Player) CalculateTSXP(level int8) float32 {
|
||||
// TODO: Implement tradeskill XP calculation
|
||||
// This is a simplified version
|
||||
|
||||
levelDiff := int(level) - int(p.GetTSLevel())
|
||||
baseXP := float32(50 + (level * 5))
|
||||
|
||||
// Level difference modifier
|
||||
var levelMod float32 = 1.0
|
||||
if levelDiff < -5 {
|
||||
levelMod = 0.1
|
||||
} else if levelDiff < -2 {
|
||||
levelMod = 0.5
|
||||
} else if levelDiff <= 2 {
|
||||
levelMod = 1.0
|
||||
} else if levelDiff <= 4 {
|
||||
levelMod = 1.2
|
||||
} else {
|
||||
levelMod = 1.5
|
||||
}
|
||||
|
||||
// Vitality modifier
|
||||
vitalityMod := float32(1.0)
|
||||
if p.GetTSXPVitality() > 0 {
|
||||
vitalityMod = 2.0
|
||||
}
|
||||
|
||||
return baseXP * levelMod * vitalityMod
|
||||
}
|
||||
|
||||
// CalculateOfflineDebtRecovery calculates debt recovery while offline
|
||||
func (p *Player) CalculateOfflineDebtRecovery(unixTimestamp int32) {
|
||||
currentTime := int32(time.Now().Unix())
|
||||
timeDiff := currentTime - unixTimestamp
|
||||
|
||||
if timeDiff <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate hours offline
|
||||
hoursOffline := float32(timeDiff) / 3600.0
|
||||
|
||||
// Debt recovery rate per hour (example: 1% per hour)
|
||||
debtRecoveryRate := float32(1.0)
|
||||
|
||||
// Calculate adventure debt recovery
|
||||
currentDebt := p.GetInfoStructXPDebt()
|
||||
if currentDebt > 0 {
|
||||
recovery := debtRecoveryRate * hoursOffline
|
||||
newDebt := currentDebt - recovery
|
||||
if newDebt < 0 {
|
||||
newDebt = 0
|
||||
}
|
||||
p.SetInfoStructXPDebt(newDebt)
|
||||
}
|
||||
|
||||
// Calculate tradeskill debt recovery
|
||||
currentTSDebt := p.GetInfoStructTSXPDebt()
|
||||
if currentTSDebt > 0 {
|
||||
recovery := debtRecoveryRate * hoursOffline
|
||||
newDebt := currentTSDebt - recovery
|
||||
if newDebt < 0 {
|
||||
newDebt = 0
|
||||
}
|
||||
p.SetInfoStructTSXPDebt(newDebt)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: GetTSLevel is now implemented in stubs.go
|
||||
|
||||
// Note: SetTSLevel is now implemented in stubs.go
|
@ -1,300 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"eq2emu/internal/entity"
|
||||
"eq2emu/internal/quests"
|
||||
"eq2emu/internal/skills"
|
||||
"eq2emu/internal/spawn"
|
||||
"eq2emu/internal/spells"
|
||||
)
|
||||
|
||||
// PlayerAware interface for components that need to interact with players
|
||||
type PlayerAware interface {
|
||||
// SetPlayer sets the player reference
|
||||
SetPlayer(player *Player)
|
||||
|
||||
// GetPlayer returns the player reference
|
||||
GetPlayer() *Player
|
||||
}
|
||||
|
||||
// PlayerManager interface for managing multiple players
|
||||
type PlayerManager interface {
|
||||
// AddPlayer adds a player to management
|
||||
AddPlayer(player *Player) error
|
||||
|
||||
// RemovePlayer removes a player from management
|
||||
RemovePlayer(playerID int32) error
|
||||
|
||||
// GetPlayer returns a player by ID
|
||||
GetPlayer(playerID int32) *Player
|
||||
|
||||
// GetPlayerByName returns a player by name
|
||||
GetPlayerByName(name string) *Player
|
||||
|
||||
// GetPlayerByCharacterID returns a player by character ID
|
||||
GetPlayerByCharacterID(characterID int32) *Player
|
||||
|
||||
// GetAllPlayers returns all managed players
|
||||
GetAllPlayers() []*Player
|
||||
|
||||
// GetPlayersInZone returns all players in a zone
|
||||
GetPlayersInZone(zoneID int32) []*Player
|
||||
|
||||
// SendToAll sends a message to all players
|
||||
SendToAll(message any) error
|
||||
|
||||
// SendToZone sends a message to all players in a zone
|
||||
SendToZone(zoneID int32, message any) error
|
||||
}
|
||||
|
||||
// PlayerDatabaseInterface interface for database operations (if needed for testing)
|
||||
type PlayerDatabaseInterface interface {
|
||||
// LoadPlayer loads a player from the database
|
||||
LoadPlayer(characterID int32) (*Player, error)
|
||||
|
||||
// SavePlayer saves a player to the database
|
||||
SavePlayer(player *Player) error
|
||||
|
||||
// DeletePlayer deletes a player from the database
|
||||
DeletePlayer(characterID int32) error
|
||||
}
|
||||
|
||||
// PlayerPacketHandler interface for handling player packets
|
||||
type PlayerPacketHandler interface {
|
||||
// HandlePacket handles a packet from a player
|
||||
HandlePacket(player *Player, packet any) error
|
||||
|
||||
// SendPacket sends a packet to a player
|
||||
SendPacket(player *Player, packet any) error
|
||||
|
||||
// BroadcastPacket broadcasts a packet to multiple players
|
||||
BroadcastPacket(players []*Player, packet any) error
|
||||
}
|
||||
|
||||
// PlayerEventHandler interface for player events
|
||||
type PlayerEventHandler interface {
|
||||
// OnPlayerLogin called when player logs in
|
||||
OnPlayerLogin(player *Player) error
|
||||
|
||||
// OnPlayerLogout called when player logs out
|
||||
OnPlayerLogout(player *Player) error
|
||||
|
||||
// OnPlayerDeath called when player dies
|
||||
OnPlayerDeath(player *Player, killer *entity.Entity) error
|
||||
|
||||
// OnPlayerResurrect called when player resurrects
|
||||
OnPlayerResurrect(player *Player) error
|
||||
|
||||
// OnPlayerLevelUp called when player levels up
|
||||
OnPlayerLevelUp(player *Player, newLevel int8) error
|
||||
|
||||
// OnPlayerZoneChange called when player changes zones
|
||||
OnPlayerZoneChange(player *Player, fromZoneID, toZoneID int32) error
|
||||
|
||||
// OnPlayerQuestComplete called when player completes a quest
|
||||
OnPlayerQuestComplete(player *Player, quest *quests.Quest) error
|
||||
|
||||
// OnPlayerSpellCast called when player casts a spell
|
||||
OnPlayerSpellCast(player *Player, spell *spells.Spell, target *entity.Entity) error
|
||||
}
|
||||
|
||||
// PlayerValidator interface for validating player operations
|
||||
type PlayerValidator interface {
|
||||
// ValidateLogin validates player login
|
||||
ValidateLogin(player *Player) error
|
||||
|
||||
// ValidateMovement validates player movement
|
||||
ValidateMovement(player *Player, x, y, z, heading float32) error
|
||||
|
||||
// ValidateSpellCast validates spell casting
|
||||
ValidateSpellCast(player *Player, spell *spells.Spell, target *entity.Entity) error
|
||||
|
||||
// ValidateItemUse validates item usage
|
||||
ValidateItemUse(player *Player, item *Item) error
|
||||
|
||||
// ValidateQuestAcceptance validates quest acceptance
|
||||
ValidateQuestAcceptance(player *Player, quest *quests.Quest) error
|
||||
|
||||
// ValidateSkillUse validates skill usage
|
||||
ValidateSkillUse(player *Player, skill *skills.Skill) error
|
||||
}
|
||||
|
||||
// PlayerSerializer interface for serializing player data
|
||||
type PlayerSerializer interface {
|
||||
// SerializePlayer serializes a player for network transmission
|
||||
SerializePlayer(player *Player, version int16) ([]byte, error)
|
||||
|
||||
// SerializePlayerInfo serializes player info for character sheet
|
||||
SerializePlayerInfo(player *Player, version int16) ([]byte, error)
|
||||
|
||||
// SerializePlayerSpells serializes player spells
|
||||
SerializePlayerSpells(player *Player, version int16) ([]byte, error)
|
||||
|
||||
// SerializePlayerQuests serializes player quests
|
||||
SerializePlayerQuests(player *Player, version int16) ([]byte, error)
|
||||
|
||||
// SerializePlayerSkills serializes player skills
|
||||
SerializePlayerSkills(player *Player, version int16) ([]byte, error)
|
||||
}
|
||||
|
||||
// PlayerStatistics interface for player statistics tracking
|
||||
type PlayerStatistics interface {
|
||||
// RecordPlayerLogin records a player login
|
||||
RecordPlayerLogin(player *Player)
|
||||
|
||||
// RecordPlayerLogout records a player logout
|
||||
RecordPlayerLogout(player *Player)
|
||||
|
||||
// RecordPlayerDeath records a player death
|
||||
RecordPlayerDeath(player *Player, killer *entity.Entity)
|
||||
|
||||
// RecordPlayerKill records a player kill
|
||||
RecordPlayerKill(player *Player, victim *entity.Entity)
|
||||
|
||||
// RecordQuestComplete records a quest completion
|
||||
RecordQuestComplete(player *Player, quest *quests.Quest)
|
||||
|
||||
// RecordSpellCast records a spell cast
|
||||
RecordSpellCast(player *Player, spell *spells.Spell)
|
||||
|
||||
// GetStatistics returns player statistics
|
||||
GetStatistics(playerID int32) map[string]any
|
||||
}
|
||||
|
||||
// PlayerNotifier interface for player notifications
|
||||
type PlayerNotifier interface {
|
||||
// NotifyLevelUp sends level up notification
|
||||
NotifyLevelUp(player *Player, newLevel int8) error
|
||||
|
||||
// NotifyQuestComplete sends quest completion notification
|
||||
NotifyQuestComplete(player *Player, quest *quests.Quest) error
|
||||
|
||||
// NotifySkillUp sends skill up notification
|
||||
NotifySkillUp(player *Player, skill *skills.Skill, newValue int16) error
|
||||
|
||||
// NotifyDeathPenalty sends death penalty notification
|
||||
NotifyDeathPenalty(player *Player, debtAmount float32) error
|
||||
|
||||
// NotifyMessage sends a general message
|
||||
NotifyMessage(player *Player, message string, messageType int8) error
|
||||
}
|
||||
|
||||
// PlayerAdapter adapts player functionality for other systems
|
||||
type PlayerAdapter struct {
|
||||
player *Player
|
||||
}
|
||||
|
||||
// NewPlayerAdapter creates a new player adapter
|
||||
func NewPlayerAdapter(player *Player) *PlayerAdapter {
|
||||
return &PlayerAdapter{player: player}
|
||||
}
|
||||
|
||||
// GetPlayer returns the wrapped player
|
||||
func (pa *PlayerAdapter) GetPlayer() *Player {
|
||||
return pa.player
|
||||
}
|
||||
|
||||
// GetEntity returns the player as an entity
|
||||
func (pa *PlayerAdapter) GetEntity() *entity.Entity {
|
||||
return &pa.player.Entity
|
||||
}
|
||||
|
||||
// GetSpawn returns the player as a spawn
|
||||
func (pa *PlayerAdapter) GetSpawn() *spawn.Spawn {
|
||||
return pa.player.Entity.Spawn
|
||||
}
|
||||
|
||||
// IsPlayer always returns true for player adapter
|
||||
func (pa *PlayerAdapter) IsPlayer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetCharacterID returns the character ID
|
||||
func (pa *PlayerAdapter) GetCharacterID() int32 {
|
||||
return pa.player.GetCharacterID()
|
||||
}
|
||||
|
||||
// GetName returns the player name
|
||||
func (pa *PlayerAdapter) GetName() string {
|
||||
return pa.player.GetName()
|
||||
}
|
||||
|
||||
// GetLevel returns the player level
|
||||
func (pa *PlayerAdapter) GetLevel() int8 {
|
||||
return pa.player.GetLevel()
|
||||
}
|
||||
|
||||
// GetClass returns the player class
|
||||
func (pa *PlayerAdapter) GetClass() int8 {
|
||||
return pa.player.GetClass()
|
||||
}
|
||||
|
||||
// GetRace returns the player race
|
||||
func (pa *PlayerAdapter) GetRace() int8 {
|
||||
return pa.player.GetRace()
|
||||
}
|
||||
|
||||
// GetZoneID returns the current zone ID
|
||||
func (pa *PlayerAdapter) GetZoneID() int32 {
|
||||
return pa.player.GetZone()
|
||||
}
|
||||
|
||||
// GetHP returns current HP
|
||||
func (pa *PlayerAdapter) GetHP() int32 {
|
||||
return pa.player.GetHP()
|
||||
}
|
||||
|
||||
// GetMaxHP returns maximum HP
|
||||
func (pa *PlayerAdapter) GetMaxHP() int32 {
|
||||
return pa.player.GetTotalHP()
|
||||
}
|
||||
|
||||
// GetPower returns current power
|
||||
func (pa *PlayerAdapter) GetPower() int32 {
|
||||
return pa.player.GetPower()
|
||||
}
|
||||
|
||||
// GetMaxPower returns maximum power
|
||||
func (pa *PlayerAdapter) GetMaxPower() int32 {
|
||||
return pa.player.GetTotalPower()
|
||||
}
|
||||
|
||||
// GetX returns X coordinate
|
||||
func (pa *PlayerAdapter) GetX() float32 {
|
||||
return pa.player.GetX()
|
||||
}
|
||||
|
||||
// GetY returns Y coordinate
|
||||
func (pa *PlayerAdapter) GetY() float32 {
|
||||
return pa.player.GetY()
|
||||
}
|
||||
|
||||
// GetZ returns Z coordinate
|
||||
func (pa *PlayerAdapter) GetZ() float32 {
|
||||
return pa.player.GetZ()
|
||||
}
|
||||
|
||||
// GetHeading returns heading
|
||||
func (pa *PlayerAdapter) GetHeading() float32 {
|
||||
return pa.player.GetHeading()
|
||||
}
|
||||
|
||||
// IsDead returns whether the player is dead
|
||||
func (pa *PlayerAdapter) IsDead() bool {
|
||||
return pa.player.IsDead()
|
||||
}
|
||||
|
||||
// IsAlive returns whether the player is alive
|
||||
func (pa *PlayerAdapter) IsAlive() bool {
|
||||
return !pa.player.IsDead()
|
||||
}
|
||||
|
||||
// IsInCombat returns whether the player is in combat
|
||||
func (pa *PlayerAdapter) IsInCombat() bool {
|
||||
return pa.player.GetPlayerEngageCommands() != 0
|
||||
}
|
||||
|
||||
// GetDistance returns distance to another spawn
|
||||
func (pa *PlayerAdapter) GetDistance(other *spawn.Spawn) float32 {
|
||||
return pa.player.GetDistance(other.GetX(), other.GetY(), other.GetZ(), true)
|
||||
}
|
@ -1,616 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/entity"
|
||||
)
|
||||
|
||||
// Manager handles player management operations
|
||||
type Manager struct {
|
||||
// Players indexed by various keys
|
||||
playersLock sync.RWMutex
|
||||
players map[int32]*Player // playerID -> Player
|
||||
playersByName map[string]*Player // name -> Player (case insensitive)
|
||||
playersByCharID map[int32]*Player // characterID -> Player
|
||||
playersByZone map[int32][]*Player // zoneID -> []*Player
|
||||
|
||||
// Player statistics
|
||||
stats PlayerStats
|
||||
statsLock sync.RWMutex
|
||||
|
||||
// Event handlers
|
||||
eventHandlers []PlayerEventHandler
|
||||
eventLock sync.RWMutex
|
||||
|
||||
// Validators
|
||||
validators []PlayerValidator
|
||||
|
||||
// Database interface
|
||||
database *PlayerDatabase
|
||||
|
||||
// Packet handler
|
||||
packetHandler PlayerPacketHandler
|
||||
|
||||
// Notifier
|
||||
notifier PlayerNotifier
|
||||
|
||||
// Statistics tracker
|
||||
statistics PlayerStatistics
|
||||
|
||||
// Configuration
|
||||
config ManagerConfig
|
||||
|
||||
// Shutdown channel
|
||||
shutdown chan struct{}
|
||||
|
||||
// Background goroutines
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// PlayerStats holds various player statistics
|
||||
type PlayerStats struct {
|
||||
TotalPlayers int64
|
||||
ActivePlayers int64
|
||||
PlayersLoggedIn int64
|
||||
PlayersLoggedOut int64
|
||||
AverageLevel float64
|
||||
MaxLevel int8
|
||||
TotalPlayTime time.Duration
|
||||
}
|
||||
|
||||
// ManagerConfig holds configuration for the player manager
|
||||
type ManagerConfig struct {
|
||||
// Maximum number of players
|
||||
MaxPlayers int32
|
||||
|
||||
// Player save interval
|
||||
SaveInterval time.Duration
|
||||
|
||||
// Statistics update interval
|
||||
StatsInterval time.Duration
|
||||
|
||||
// Enable player validation
|
||||
EnableValidation bool
|
||||
|
||||
// Enable event handling
|
||||
EnableEvents bool
|
||||
|
||||
// Enable statistics tracking
|
||||
EnableStatistics bool
|
||||
}
|
||||
|
||||
// NewManager creates a new player manager
|
||||
func NewManager(config ManagerConfig) *Manager {
|
||||
return &Manager{
|
||||
players: make(map[int32]*Player),
|
||||
playersByName: make(map[string]*Player),
|
||||
playersByCharID: make(map[int32]*Player),
|
||||
playersByZone: make(map[int32][]*Player),
|
||||
eventHandlers: make([]PlayerEventHandler, 0),
|
||||
validators: make([]PlayerValidator, 0),
|
||||
config: config,
|
||||
shutdown: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the player manager
|
||||
func (m *Manager) Start() error {
|
||||
// Start background processes
|
||||
if m.config.SaveInterval > 0 {
|
||||
m.wg.Add(1)
|
||||
go m.savePlayersLoop()
|
||||
}
|
||||
|
||||
if m.config.StatsInterval > 0 {
|
||||
m.wg.Add(1)
|
||||
go m.updateStatsLoop()
|
||||
}
|
||||
|
||||
m.wg.Add(1)
|
||||
go m.processPlayersLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the player manager
|
||||
func (m *Manager) Stop() error {
|
||||
close(m.shutdown)
|
||||
m.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddPlayer adds a player to management
|
||||
func (m *Manager) AddPlayer(player *Player) error {
|
||||
if player == nil {
|
||||
return fmt.Errorf("player cannot be nil")
|
||||
}
|
||||
|
||||
m.playersLock.Lock()
|
||||
defer m.playersLock.Unlock()
|
||||
|
||||
// Check if we're at capacity
|
||||
if m.config.MaxPlayers > 0 && int32(len(m.players)) >= m.config.MaxPlayers {
|
||||
return fmt.Errorf("server at maximum player capacity")
|
||||
}
|
||||
|
||||
playerID := player.GetSpawnID()
|
||||
characterID := player.GetCharacterID()
|
||||
name := strings.TrimSpace(strings.Trim(player.GetName(), "\x00")) // Trim padding and null bytes
|
||||
zoneID := player.GetZone()
|
||||
|
||||
// Check for duplicates
|
||||
if _, exists := m.players[playerID]; exists {
|
||||
return fmt.Errorf("player with ID %d already exists", playerID)
|
||||
}
|
||||
|
||||
if _, exists := m.playersByCharID[characterID]; exists {
|
||||
return fmt.Errorf("player with character ID %d already exists", characterID)
|
||||
}
|
||||
|
||||
if _, exists := m.playersByName[name]; exists {
|
||||
return fmt.Errorf("player with name %s already exists", name)
|
||||
}
|
||||
|
||||
// Add to maps
|
||||
m.players[playerID] = player
|
||||
m.playersByCharID[characterID] = player
|
||||
m.playersByName[name] = player
|
||||
|
||||
// Add to zone map
|
||||
if m.playersByZone[zoneID] == nil {
|
||||
m.playersByZone[zoneID] = make([]*Player, 0)
|
||||
}
|
||||
m.playersByZone[zoneID] = append(m.playersByZone[zoneID], player)
|
||||
|
||||
// Update statistics
|
||||
m.updateStatsForAdd()
|
||||
|
||||
// Fire event
|
||||
if m.config.EnableEvents {
|
||||
m.firePlayerLoginEvent(player)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePlayer removes a player from management
|
||||
func (m *Manager) RemovePlayer(playerID int32) error {
|
||||
m.playersLock.Lock()
|
||||
defer m.playersLock.Unlock()
|
||||
|
||||
player, exists := m.players[playerID]
|
||||
if !exists {
|
||||
return fmt.Errorf("player with ID %d not found", playerID)
|
||||
}
|
||||
|
||||
// Remove from maps
|
||||
delete(m.players, playerID)
|
||||
delete(m.playersByCharID, player.GetCharacterID())
|
||||
name := strings.TrimSpace(strings.Trim(player.GetName(), "\x00"))
|
||||
delete(m.playersByName, name)
|
||||
|
||||
// Remove from zone map
|
||||
zoneID := player.GetZone()
|
||||
if zonePlayers, exists := m.playersByZone[zoneID]; exists {
|
||||
for i, p := range zonePlayers {
|
||||
if p == player {
|
||||
m.playersByZone[zoneID] = append(zonePlayers[:i], zonePlayers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Clean up empty zone lists
|
||||
if len(m.playersByZone[zoneID]) == 0 {
|
||||
delete(m.playersByZone, zoneID)
|
||||
}
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
m.updateStatsForRemove()
|
||||
|
||||
// Fire event
|
||||
if m.config.EnableEvents {
|
||||
m.firePlayerLogoutEvent(player)
|
||||
}
|
||||
|
||||
// Save player data before removal
|
||||
if m.database != nil {
|
||||
m.database.SavePlayer(player)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlayer returns a player by spawn ID
|
||||
func (m *Manager) GetPlayer(playerID int32) *Player {
|
||||
m.playersLock.RLock()
|
||||
defer m.playersLock.RUnlock()
|
||||
|
||||
return m.players[playerID]
|
||||
}
|
||||
|
||||
// GetPlayerByName returns a player by name
|
||||
func (m *Manager) GetPlayerByName(name string) *Player {
|
||||
m.playersLock.RLock()
|
||||
defer m.playersLock.RUnlock()
|
||||
|
||||
return m.playersByName[strings.TrimSpace(strings.Trim(name, "\x00"))]
|
||||
}
|
||||
|
||||
// GetPlayerByCharacterID returns a player by character ID
|
||||
func (m *Manager) GetPlayerByCharacterID(characterID int32) *Player {
|
||||
m.playersLock.RLock()
|
||||
defer m.playersLock.RUnlock()
|
||||
|
||||
return m.playersByCharID[characterID]
|
||||
}
|
||||
|
||||
// GetAllPlayers returns all managed players
|
||||
func (m *Manager) GetAllPlayers() []*Player {
|
||||
m.playersLock.RLock()
|
||||
defer m.playersLock.RUnlock()
|
||||
|
||||
players := make([]*Player, 0, len(m.players))
|
||||
for _, player := range m.players {
|
||||
players = append(players, player)
|
||||
}
|
||||
return players
|
||||
}
|
||||
|
||||
// GetPlayersInZone returns all players in a zone
|
||||
func (m *Manager) GetPlayersInZone(zoneID int32) []*Player {
|
||||
m.playersLock.RLock()
|
||||
defer m.playersLock.RUnlock()
|
||||
|
||||
if zonePlayers, exists := m.playersByZone[zoneID]; exists {
|
||||
// Return a copy to avoid race conditions
|
||||
players := make([]*Player, len(zonePlayers))
|
||||
copy(players, zonePlayers)
|
||||
return players
|
||||
}
|
||||
|
||||
return []*Player{}
|
||||
}
|
||||
|
||||
// SendToAll sends a message to all players
|
||||
func (m *Manager) SendToAll(message any) error {
|
||||
if m.packetHandler == nil {
|
||||
return fmt.Errorf("no packet handler configured")
|
||||
}
|
||||
|
||||
players := m.GetAllPlayers()
|
||||
return m.packetHandler.BroadcastPacket(players, message)
|
||||
}
|
||||
|
||||
// SendToZone sends a message to all players in a zone
|
||||
func (m *Manager) SendToZone(zoneID int32, message any) error {
|
||||
if m.packetHandler == nil {
|
||||
return fmt.Errorf("no packet handler configured")
|
||||
}
|
||||
|
||||
players := m.GetPlayersInZone(zoneID)
|
||||
return m.packetHandler.BroadcastPacket(players, message)
|
||||
}
|
||||
|
||||
// MovePlayerToZone moves a player to a different zone
|
||||
func (m *Manager) MovePlayerToZone(playerID, newZoneID int32) error {
|
||||
m.playersLock.Lock()
|
||||
defer m.playersLock.Unlock()
|
||||
|
||||
player, exists := m.players[playerID]
|
||||
if !exists {
|
||||
return fmt.Errorf("player with ID %d not found", playerID)
|
||||
}
|
||||
|
||||
oldZoneID := player.GetZone()
|
||||
if oldZoneID == newZoneID {
|
||||
return nil // Already in the zone
|
||||
}
|
||||
|
||||
// Remove from old zone
|
||||
if zonePlayers, exists := m.playersByZone[oldZoneID]; exists {
|
||||
for i, p := range zonePlayers {
|
||||
if p == player {
|
||||
m.playersByZone[oldZoneID] = append(zonePlayers[:i], zonePlayers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(m.playersByZone[oldZoneID]) == 0 {
|
||||
delete(m.playersByZone, oldZoneID)
|
||||
}
|
||||
}
|
||||
|
||||
// Add to new zone
|
||||
if m.playersByZone[newZoneID] == nil {
|
||||
m.playersByZone[newZoneID] = make([]*Player, 0)
|
||||
}
|
||||
m.playersByZone[newZoneID] = append(m.playersByZone[newZoneID], player)
|
||||
|
||||
// Update player's zone
|
||||
player.SetZone(newZoneID)
|
||||
|
||||
// Fire event
|
||||
if m.config.EnableEvents {
|
||||
m.firePlayerZoneChangeEvent(player, oldZoneID, newZoneID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlayerCount returns the current number of players
|
||||
func (m *Manager) GetPlayerCount() int32 {
|
||||
m.playersLock.RLock()
|
||||
defer m.playersLock.RUnlock()
|
||||
|
||||
return int32(len(m.players))
|
||||
}
|
||||
|
||||
// GetZonePlayerCount returns the number of players in a zone
|
||||
func (m *Manager) GetZonePlayerCount(zoneID int32) int32 {
|
||||
m.playersLock.RLock()
|
||||
defer m.playersLock.RUnlock()
|
||||
|
||||
if zonePlayers, exists := m.playersByZone[zoneID]; exists {
|
||||
return int32(len(zonePlayers))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetPlayerStats returns current player statistics
|
||||
func (m *Manager) GetPlayerStats() PlayerStats {
|
||||
m.statsLock.RLock()
|
||||
defer m.statsLock.RUnlock()
|
||||
|
||||
return m.stats
|
||||
}
|
||||
|
||||
// AddEventHandler adds an event handler
|
||||
func (m *Manager) AddEventHandler(handler PlayerEventHandler) {
|
||||
m.eventLock.Lock()
|
||||
defer m.eventLock.Unlock()
|
||||
|
||||
m.eventHandlers = append(m.eventHandlers, handler)
|
||||
}
|
||||
|
||||
// AddValidator adds a validator
|
||||
func (m *Manager) AddValidator(validator PlayerValidator) {
|
||||
m.validators = append(m.validators, validator)
|
||||
}
|
||||
|
||||
// SetDatabase sets the database interface
|
||||
func (m *Manager) SetDatabase(db *PlayerDatabase) {
|
||||
m.database = db
|
||||
}
|
||||
|
||||
// SetPacketHandler sets the packet handler
|
||||
func (m *Manager) SetPacketHandler(handler PlayerPacketHandler) {
|
||||
m.packetHandler = handler
|
||||
}
|
||||
|
||||
// SetNotifier sets the notifier
|
||||
func (m *Manager) SetNotifier(notifier PlayerNotifier) {
|
||||
m.notifier = notifier
|
||||
}
|
||||
|
||||
// SetStatistics sets the statistics tracker
|
||||
func (m *Manager) SetStatistics(stats PlayerStatistics) {
|
||||
m.statistics = stats
|
||||
}
|
||||
|
||||
// ValidatePlayer validates a player using all validators
|
||||
func (m *Manager) ValidatePlayer(player *Player) error {
|
||||
if !m.config.EnableValidation {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, validator := range m.validators {
|
||||
if err := validator.ValidateLogin(player); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// savePlayersLoop periodically saves all players
|
||||
func (m *Manager) savePlayersLoop() {
|
||||
defer m.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(m.config.SaveInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.saveAllPlayers()
|
||||
case <-m.shutdown:
|
||||
// Final save before shutdown
|
||||
m.saveAllPlayers()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateStatsLoop periodically updates statistics
|
||||
func (m *Manager) updateStatsLoop() {
|
||||
defer m.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(m.config.StatsInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.updatePlayerStats()
|
||||
case <-m.shutdown:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processPlayersLoop processes player updates
|
||||
func (m *Manager) processPlayersLoop() {
|
||||
defer m.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(100 * time.Millisecond) // 10Hz
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.processAllPlayers()
|
||||
case <-m.shutdown:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveAllPlayers saves all players to database
|
||||
func (m *Manager) saveAllPlayers() {
|
||||
if m.database == nil {
|
||||
return
|
||||
}
|
||||
|
||||
players := m.GetAllPlayers()
|
||||
for _, player := range players {
|
||||
m.database.SavePlayer(player)
|
||||
}
|
||||
}
|
||||
|
||||
// updatePlayerStats updates player statistics
|
||||
func (m *Manager) updatePlayerStats() {
|
||||
m.playersLock.RLock()
|
||||
defer m.playersLock.RUnlock()
|
||||
|
||||
m.statsLock.Lock()
|
||||
defer m.statsLock.Unlock()
|
||||
|
||||
m.stats.ActivePlayers = int64(len(m.players))
|
||||
|
||||
var totalLevel int64
|
||||
var maxLevel int8
|
||||
|
||||
for _, player := range m.players {
|
||||
level := player.GetLevel()
|
||||
totalLevel += int64(level)
|
||||
if level > maxLevel {
|
||||
maxLevel = level
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.players) > 0 {
|
||||
m.stats.AverageLevel = float64(totalLevel) / float64(len(m.players))
|
||||
}
|
||||
m.stats.MaxLevel = maxLevel
|
||||
}
|
||||
|
||||
// processAllPlayers processes updates for all players
|
||||
func (m *Manager) processAllPlayers() {
|
||||
players := m.GetAllPlayers()
|
||||
|
||||
for _, player := range players {
|
||||
// Process spawn state queue
|
||||
player.CheckSpawnStateQueue()
|
||||
|
||||
// Process combat
|
||||
player.ProcessCombat()
|
||||
|
||||
// Process range updates
|
||||
player.ProcessSpawnRangeUpdates()
|
||||
|
||||
// TODO: Add other periodic processing
|
||||
}
|
||||
}
|
||||
|
||||
// updateStatsForAdd updates stats when a player is added
|
||||
func (m *Manager) updateStatsForAdd() {
|
||||
m.statsLock.Lock()
|
||||
defer m.statsLock.Unlock()
|
||||
|
||||
m.stats.TotalPlayers++
|
||||
m.stats.PlayersLoggedIn++
|
||||
}
|
||||
|
||||
// updateStatsForRemove updates stats when a player is removed
|
||||
func (m *Manager) updateStatsForRemove() {
|
||||
m.statsLock.Lock()
|
||||
defer m.statsLock.Unlock()
|
||||
|
||||
m.stats.PlayersLoggedOut++
|
||||
}
|
||||
|
||||
// Event firing methods
|
||||
func (m *Manager) firePlayerLoginEvent(player *Player) {
|
||||
m.eventLock.RLock()
|
||||
defer m.eventLock.RUnlock()
|
||||
|
||||
for _, handler := range m.eventHandlers {
|
||||
handler.OnPlayerLogin(player)
|
||||
}
|
||||
|
||||
if m.statistics != nil {
|
||||
m.statistics.RecordPlayerLogin(player)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) firePlayerLogoutEvent(player *Player) {
|
||||
m.eventLock.RLock()
|
||||
defer m.eventLock.RUnlock()
|
||||
|
||||
for _, handler := range m.eventHandlers {
|
||||
handler.OnPlayerLogout(player)
|
||||
}
|
||||
|
||||
if m.statistics != nil {
|
||||
m.statistics.RecordPlayerLogout(player)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) firePlayerZoneChangeEvent(player *Player, fromZoneID, toZoneID int32) {
|
||||
m.eventLock.RLock()
|
||||
defer m.eventLock.RUnlock()
|
||||
|
||||
for _, handler := range m.eventHandlers {
|
||||
handler.OnPlayerZoneChange(player, fromZoneID, toZoneID)
|
||||
}
|
||||
}
|
||||
|
||||
// FirePlayerLevelUpEvent fires a level up event
|
||||
func (m *Manager) FirePlayerLevelUpEvent(player *Player, newLevel int8) {
|
||||
m.eventLock.RLock()
|
||||
defer m.eventLock.RUnlock()
|
||||
|
||||
for _, handler := range m.eventHandlers {
|
||||
handler.OnPlayerLevelUp(player, newLevel)
|
||||
}
|
||||
|
||||
if m.notifier != nil {
|
||||
m.notifier.NotifyLevelUp(player, newLevel)
|
||||
}
|
||||
}
|
||||
|
||||
// FirePlayerDeathEvent fires a death event
|
||||
func (m *Manager) FirePlayerDeathEvent(player *Player, killer *entity.Entity) {
|
||||
m.eventLock.RLock()
|
||||
defer m.eventLock.RUnlock()
|
||||
|
||||
for _, handler := range m.eventHandlers {
|
||||
handler.OnPlayerDeath(player, killer)
|
||||
}
|
||||
|
||||
if m.statistics != nil {
|
||||
m.statistics.RecordPlayerDeath(player, killer)
|
||||
}
|
||||
}
|
||||
|
||||
// FirePlayerResurrectEvent fires a resurrect event
|
||||
func (m *Manager) FirePlayerResurrectEvent(player *Player) {
|
||||
m.eventLock.RLock()
|
||||
defer m.eventLock.RUnlock()
|
||||
|
||||
for _, handler := range m.eventHandlers {
|
||||
handler.OnPlayerResurrect(player)
|
||||
}
|
||||
}
|
@ -13,6 +13,9 @@ import (
|
||||
var levelXPReq map[int8]int32
|
||||
var xpTableOnce sync.Once
|
||||
|
||||
// Global movement data storage (TODO: move to proper entity system)
|
||||
var playerMovementData = make(map[int32]map[string]float32)
|
||||
|
||||
// NewPlayer creates a new player instance
|
||||
func NewPlayer() *Player {
|
||||
p := &Player{
|
||||
@ -73,9 +76,9 @@ func NewPlayer() *Player {
|
||||
// Set default away message
|
||||
p.awayMessage = "Sorry, I am A.F.K. (Away From Keyboard)"
|
||||
|
||||
// Add player-specific commands
|
||||
p.AddSecondaryEntityCommand("Inspect", 10000, "inspect_player", "", 0, 0)
|
||||
p.AddSecondaryEntityCommand("Who", 10000, "who", "", 0, 0)
|
||||
// Add player-specific commands (TODO: implement AddSecondaryEntityCommand)
|
||||
// p.AddSecondaryEntityCommand("Inspect", 10000, "inspect_player", "", 0, 0)
|
||||
// p.AddSecondaryEntityCommand("Who", 10000, "who", "", 0, 0)
|
||||
|
||||
// Initialize self in spawn maps
|
||||
p.playerSpawnIDMap[1] = p.Entity.Spawn
|
||||
@ -114,7 +117,9 @@ func (p *Player) SetClient(client *Client) {
|
||||
// GetPlayerInfo returns the player's info structure, creating it if needed
|
||||
func (p *Player) GetPlayerInfo() *PlayerInfo {
|
||||
if p.info == nil {
|
||||
p.info = NewPlayerInfo(p)
|
||||
p.info = &PlayerInfo{
|
||||
player: p,
|
||||
}
|
||||
}
|
||||
return p.info
|
||||
}
|
||||
@ -237,6 +242,15 @@ func (p *Player) SetSideSpeed(sideSpeed float32, updateFlags bool) {
|
||||
playerMovementData[charID]["side_speed"] = sideSpeed
|
||||
}
|
||||
|
||||
// GetPos returns a position/movement value for the player
|
||||
func (p *Player) GetPos(key string) float32 {
|
||||
charID := p.GetCharacterID()
|
||||
if playerMovementData[charID] != nil {
|
||||
return playerMovementData[charID][key]
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// GetSideSpeed returns the player's side movement speed
|
||||
func (p *Player) GetSideSpeed() float32 {
|
||||
return p.GetPos("side_speed")
|
||||
@ -518,6 +532,16 @@ func (p *Player) ResetMentorship() bool {
|
||||
return mentorshipStatus
|
||||
}
|
||||
|
||||
// SetMentorStats sets mentorship statistics (placeholder implementation)
|
||||
func (p *Player) SetMentorStats(level int32, unused1 int32, enabled bool) {
|
||||
// TODO: Implement proper mentorship stats when system is available
|
||||
}
|
||||
|
||||
// InCombat sets the combat state
|
||||
func (p *Player) InCombat(inCombat bool, rangedCombat bool) {
|
||||
// TODO: Implement proper combat state management
|
||||
}
|
||||
|
||||
// EnableResetMentorship enables mentorship reset
|
||||
func (p *Player) EnableResetMentorship() {
|
||||
p.resetMentorship = true
|
||||
|
@ -1,170 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"eq2emu/internal/spawn"
|
||||
)
|
||||
|
||||
// NewPlayerInfo creates a new PlayerInfo instance
|
||||
func NewPlayerInfo(player *Player) *PlayerInfo {
|
||||
return &PlayerInfo{
|
||||
player: player,
|
||||
infoStruct: player.GetInfoStruct(),
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateXPPercentages calculates XP bar percentages for display
|
||||
func (pi *PlayerInfo) CalculateXPPercentages() {
|
||||
xpNeeded := int32(pi.player.GetInfoStructXPNeeded())
|
||||
if xpNeeded > 0 {
|
||||
divPercent := (pi.player.GetInfoStructXP() / float64(xpNeeded)) * 100.0
|
||||
percentage := int16(divPercent) * 10
|
||||
whole := math.Floor(divPercent)
|
||||
fractional := divPercent - whole
|
||||
|
||||
pi.player.SetInfoStructXPYellow(percentage)
|
||||
pi.player.SetInfoStructXPBlue(int16(fractional * 1000))
|
||||
|
||||
// Vitality bars probably need a revisit
|
||||
pi.player.SetInfoStructXPBlueVitalityBar(0)
|
||||
pi.player.SetInfoStructXPYellowVitalityBar(0)
|
||||
|
||||
if pi.player.GetXPVitality() > 0 {
|
||||
vitalityTotal := pi.player.GetXPVitality()*10 + float32(percentage)
|
||||
vitalityTotal -= float32((int(percentage/100) * 100))
|
||||
if vitalityTotal < 100 { // 10%
|
||||
pi.player.SetInfoStructXPBlueVitalityBar(pi.player.GetInfoStructXPBlue() + int16(pi.player.GetXPVitality()*10))
|
||||
} else {
|
||||
pi.player.SetInfoStructXPYellowVitalityBar(pi.player.GetInfoStructXPYellow() + int16(pi.player.GetXPVitality()*10))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateTSXPPercentages calculates tradeskill XP bar percentages
|
||||
func (pi *PlayerInfo) CalculateTSXPPercentages() {
|
||||
tsXPNeeded := int32(pi.player.GetInfoStructTSXPNeeded())
|
||||
if tsXPNeeded > 0 {
|
||||
percentage := (pi.player.GetInfoStructTSXP() / float64(tsXPNeeded)) * 1000
|
||||
pi.player.SetInfoStructTradeskillExpYellow(int16(percentage))
|
||||
pi.player.SetInfoStructTradeskillExpBlue(int16((percentage - float64(pi.player.GetInfoStructTradeskillExpYellow())) * 1000))
|
||||
}
|
||||
}
|
||||
|
||||
// SetHouseZone sets the house zone ID
|
||||
func (pi *PlayerInfo) SetHouseZone(id int32) {
|
||||
pi.houseZoneID = id
|
||||
}
|
||||
|
||||
// SetBindZone sets the bind zone ID
|
||||
func (pi *PlayerInfo) SetBindZone(id int32) {
|
||||
pi.bindZoneID = id
|
||||
}
|
||||
|
||||
// SetBindX sets the bind X coordinate
|
||||
func (pi *PlayerInfo) SetBindX(x float32) {
|
||||
pi.bindX = x
|
||||
}
|
||||
|
||||
// SetBindY sets the bind Y coordinate
|
||||
func (pi *PlayerInfo) SetBindY(y float32) {
|
||||
pi.bindY = y
|
||||
}
|
||||
|
||||
// SetBindZ sets the bind Z coordinate
|
||||
func (pi *PlayerInfo) SetBindZ(z float32) {
|
||||
pi.bindZ = z
|
||||
}
|
||||
|
||||
// SetBindHeading sets the bind heading
|
||||
func (pi *PlayerInfo) SetBindHeading(heading float32) {
|
||||
pi.bindHeading = heading
|
||||
}
|
||||
|
||||
// GetHouseZoneID returns the house zone ID
|
||||
func (pi *PlayerInfo) GetHouseZoneID() int32 {
|
||||
return pi.houseZoneID
|
||||
}
|
||||
|
||||
// GetBindZoneID returns the bind zone ID
|
||||
func (pi *PlayerInfo) GetBindZoneID() int32 {
|
||||
return pi.bindZoneID
|
||||
}
|
||||
|
||||
// GetBindZoneX returns the bind X coordinate
|
||||
func (pi *PlayerInfo) GetBindZoneX() float32 {
|
||||
return pi.bindX
|
||||
}
|
||||
|
||||
// GetBindZoneY returns the bind Y coordinate
|
||||
func (pi *PlayerInfo) GetBindZoneY() float32 {
|
||||
return pi.bindY
|
||||
}
|
||||
|
||||
// GetBindZoneZ returns the bind Z coordinate
|
||||
func (pi *PlayerInfo) GetBindZoneZ() float32 {
|
||||
return pi.bindZ
|
||||
}
|
||||
|
||||
// GetBindZoneHeading returns the bind heading
|
||||
func (pi *PlayerInfo) GetBindZoneHeading() float32 {
|
||||
return pi.bindHeading
|
||||
}
|
||||
|
||||
// GetBoatX returns the boat X offset
|
||||
func (pi *PlayerInfo) GetBoatX() float32 {
|
||||
return pi.boatXOffset
|
||||
}
|
||||
|
||||
// GetBoatY returns the boat Y offset
|
||||
func (pi *PlayerInfo) GetBoatY() float32 {
|
||||
return pi.boatYOffset
|
||||
}
|
||||
|
||||
// GetBoatZ returns the boat Z offset
|
||||
func (pi *PlayerInfo) GetBoatZ() float32 {
|
||||
return pi.boatZOffset
|
||||
}
|
||||
|
||||
// GetBoatSpawn returns the boat spawn ID
|
||||
func (pi *PlayerInfo) GetBoatSpawn() int32 {
|
||||
return pi.boatSpawn
|
||||
}
|
||||
|
||||
// SetBoatX sets the boat X offset
|
||||
func (pi *PlayerInfo) SetBoatX(x float32) {
|
||||
pi.boatXOffset = x
|
||||
}
|
||||
|
||||
// SetBoatY sets the boat Y offset
|
||||
func (pi *PlayerInfo) SetBoatY(y float32) {
|
||||
pi.boatYOffset = y
|
||||
}
|
||||
|
||||
// SetBoatZ sets the boat Z offset
|
||||
func (pi *PlayerInfo) SetBoatZ(z float32) {
|
||||
pi.boatZOffset = z
|
||||
}
|
||||
|
||||
// SetBoatSpawn sets the boat spawn
|
||||
func (pi *PlayerInfo) SetBoatSpawn(spawn *spawn.Spawn) {
|
||||
if spawn != nil {
|
||||
pi.boatSpawn = spawn.GetDatabaseID()
|
||||
} else {
|
||||
pi.boatSpawn = 0
|
||||
}
|
||||
}
|
||||
|
||||
// SetAccountAge sets the account age base
|
||||
func (pi *PlayerInfo) SetAccountAge(age int32) {
|
||||
pi.player.SetInfoStructAccountAgeBase(age)
|
||||
}
|
||||
|
||||
// RemoveOldPackets cleans up old packet data
|
||||
func (pi *PlayerInfo) RemoveOldPackets() {
|
||||
pi.changes = nil
|
||||
pi.origPacket = nil
|
||||
pi.petChanges = nil
|
||||
pi.petOrigPacket = nil
|
||||
}
|
@ -1,410 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"eq2emu/internal/quests"
|
||||
"eq2emu/internal/spawn"
|
||||
"eq2emu/internal/spells"
|
||||
)
|
||||
|
||||
// GetQuest returns a quest by ID
|
||||
func (p *Player) GetQuest(questID int32) *quests.Quest {
|
||||
p.playerQuestsMutex.RLock()
|
||||
defer p.playerQuestsMutex.RUnlock()
|
||||
|
||||
if quest, exists := p.playerQuests[questID]; exists {
|
||||
return quest
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAnyQuest returns a quest from any list (active, completed, pending)
|
||||
func (p *Player) GetAnyQuest(questID int32) *quests.Quest {
|
||||
p.playerQuestsMutex.RLock()
|
||||
defer p.playerQuestsMutex.RUnlock()
|
||||
|
||||
// Check active quests
|
||||
if quest, exists := p.playerQuests[questID]; exists {
|
||||
return quest
|
||||
}
|
||||
|
||||
// Check completed quests
|
||||
if quest, exists := p.completedQuests[questID]; exists {
|
||||
return quest
|
||||
}
|
||||
|
||||
// Check pending quests
|
||||
if quest, exists := p.pendingQuests[questID]; exists {
|
||||
return quest
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCompletedQuest returns a completed quest by ID
|
||||
func (p *Player) GetCompletedQuest(questID int32) *quests.Quest {
|
||||
p.playerQuestsMutex.RLock()
|
||||
defer p.playerQuestsMutex.RUnlock()
|
||||
|
||||
if quest, exists := p.completedQuests[questID]; exists {
|
||||
return quest
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasQuestBeenCompleted checks if a quest has been completed
|
||||
func (p *Player) HasQuestBeenCompleted(questID int32) bool {
|
||||
return p.GetCompletedQuest(questID) != nil
|
||||
}
|
||||
|
||||
// GetQuestCompletedCount returns how many times a quest has been completed
|
||||
func (p *Player) GetQuestCompletedCount(questID int32) int32 {
|
||||
quest := p.GetCompletedQuest(questID)
|
||||
if quest != nil {
|
||||
return GetQuestCompleteCount(quest)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// AddCompletedQuest adds a quest to the completed list
|
||||
func (p *Player) AddCompletedQuest(quest *quests.Quest) {
|
||||
if quest == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.playerQuestsMutex.Lock()
|
||||
defer p.playerQuestsMutex.Unlock()
|
||||
|
||||
p.completedQuests[GetQuestID(quest)] = quest
|
||||
}
|
||||
|
||||
// HasActiveQuest checks if a quest is currently active
|
||||
func (p *Player) HasActiveQuest(questID int32) bool {
|
||||
p.playerQuestsMutex.RLock()
|
||||
defer p.playerQuestsMutex.RUnlock()
|
||||
|
||||
_, exists := p.playerQuests[questID]
|
||||
return exists
|
||||
}
|
||||
|
||||
// HasAnyQuest checks if player has quest in any state
|
||||
func (p *Player) HasAnyQuest(questID int32) bool {
|
||||
return p.GetAnyQuest(questID) != nil
|
||||
}
|
||||
|
||||
// GetPlayerQuests returns the active quest map
|
||||
func (p *Player) GetPlayerQuests() map[int32]*quests.Quest {
|
||||
return p.playerQuests
|
||||
}
|
||||
|
||||
// GetCompletedPlayerQuests returns the completed quest map
|
||||
func (p *Player) GetCompletedPlayerQuests() map[int32]*quests.Quest {
|
||||
return p.completedQuests
|
||||
}
|
||||
|
||||
// GetQuestIDs returns all active quest IDs
|
||||
func (p *Player) GetQuestIDs() []int32 {
|
||||
p.playerQuestsMutex.RLock()
|
||||
defer p.playerQuestsMutex.RUnlock()
|
||||
|
||||
ids := make([]int32, 0, len(p.playerQuests))
|
||||
for id := range p.playerQuests {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// RemoveQuest removes a quest from the player
|
||||
// If completeQuest is true, the quest is moved to completed list
|
||||
func (p *Player) RemoveQuest(questID int32, completeQuest bool) {
|
||||
p.playerQuestsMutex.Lock()
|
||||
defer p.playerQuestsMutex.Unlock()
|
||||
|
||||
if quest, exists := p.playerQuests[questID]; exists {
|
||||
delete(p.playerQuests, questID)
|
||||
|
||||
if completeQuest {
|
||||
// Move quest to completed list
|
||||
p.completedQuests[questID] = quest
|
||||
// Update completion count
|
||||
IncrementQuestCompleteCount(quest)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Update quest journal
|
||||
// TODO: Remove quest items if needed
|
||||
}
|
||||
|
||||
// AddQuestRequiredSpawn adds a spawn requirement for a quest
|
||||
func (p *Player) AddQuestRequiredSpawn(spawn *spawn.Spawn, questID int32) {
|
||||
if spawn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.playerSpawnQuestsRequiredMutex.Lock()
|
||||
defer p.playerSpawnQuestsRequiredMutex.Unlock()
|
||||
|
||||
spawnID := spawn.GetDatabaseID()
|
||||
if p.playerSpawnQuestsRequired[spawnID] == nil {
|
||||
p.playerSpawnQuestsRequired[spawnID] = make([]int32, 0)
|
||||
}
|
||||
|
||||
// Check if already added
|
||||
for _, id := range p.playerSpawnQuestsRequired[spawnID] {
|
||||
if id == questID {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p.playerSpawnQuestsRequired[spawnID] = append(p.playerSpawnQuestsRequired[spawnID], questID)
|
||||
}
|
||||
|
||||
// AddHistoryRequiredSpawn adds a spawn requirement for history
|
||||
func (p *Player) AddHistoryRequiredSpawn(spawn *spawn.Spawn, eventID int32) {
|
||||
if spawn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.playerSpawnHistoryRequiredMutex.Lock()
|
||||
defer p.playerSpawnHistoryRequiredMutex.Unlock()
|
||||
|
||||
spawnID := spawn.GetDatabaseID()
|
||||
if p.playerSpawnHistoryRequired[spawnID] == nil {
|
||||
p.playerSpawnHistoryRequired[spawnID] = make([]int32, 0)
|
||||
}
|
||||
|
||||
// Check if already added
|
||||
for _, id := range p.playerSpawnHistoryRequired[spawnID] {
|
||||
if id == eventID {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p.playerSpawnHistoryRequired[spawnID] = append(p.playerSpawnHistoryRequired[spawnID], eventID)
|
||||
}
|
||||
|
||||
// CheckQuestRequired checks if a spawn is required for any quest
|
||||
func (p *Player) CheckQuestRequired(spawn *spawn.Spawn) bool {
|
||||
if spawn == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
p.playerSpawnQuestsRequiredMutex.RLock()
|
||||
defer p.playerSpawnQuestsRequiredMutex.RUnlock()
|
||||
|
||||
spawnID := spawn.GetDatabaseID()
|
||||
quests, exists := p.playerSpawnQuestsRequired[spawnID]
|
||||
return exists && len(quests) > 0
|
||||
}
|
||||
|
||||
// GetQuestStepComplete checks if a quest step is complete
|
||||
func (p *Player) GetQuestStepComplete(questID, stepID int32) bool {
|
||||
quest := p.GetQuest(questID)
|
||||
if quest != nil {
|
||||
return quest.GetQuestStepCompleted(stepID)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetQuestStep returns the current quest step
|
||||
func (p *Player) GetQuestStep(questID int32) int16 {
|
||||
quest := p.GetQuest(questID)
|
||||
if quest != nil {
|
||||
return GetQuestStep(quest)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetTaskGroupStep returns the current task group step
|
||||
func (p *Player) GetTaskGroupStep(questID int32) int16 {
|
||||
quest := p.GetQuest(questID)
|
||||
if quest != nil {
|
||||
return int16(GetQuestTaskGroup(quest))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// SetStepComplete completes a quest step
|
||||
func (p *Player) SetStepComplete(questID, step int32) *quests.Quest {
|
||||
quest := p.GetQuest(questID)
|
||||
if quest != nil {
|
||||
quest.SetStepComplete(step)
|
||||
// TODO: Check if quest is now complete
|
||||
// TODO: Send quest update
|
||||
}
|
||||
return quest
|
||||
}
|
||||
|
||||
// AddStepProgress adds progress to a quest step
|
||||
func (p *Player) AddStepProgress(questID, step, progress int32) *quests.Quest {
|
||||
quest := p.GetQuest(questID)
|
||||
if quest != nil {
|
||||
quest.AddStepProgress(step, progress)
|
||||
// TODO: Check if step is now complete
|
||||
// TODO: Send quest update
|
||||
}
|
||||
return quest
|
||||
}
|
||||
|
||||
// GetStepProgress returns progress for a quest step
|
||||
func (p *Player) GetStepProgress(questID, stepID int32) int32 {
|
||||
quest := p.GetQuest(questID)
|
||||
if quest != nil {
|
||||
return quest.GetStepProgress(stepID)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// CanReceiveQuest checks if player can receive a quest
|
||||
func (p *Player) CanReceiveQuest(questID int32, ret *int8) bool {
|
||||
// TODO: Get quest from master list
|
||||
// quest := master_quest_list.GetQuest(questID)
|
||||
|
||||
// Check if already has quest
|
||||
if p.HasAnyQuest(questID) {
|
||||
if ret != nil {
|
||||
*ret = 1 // Already has quest
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: Check prerequisites
|
||||
// - Level requirements
|
||||
// - Class requirements
|
||||
// - Race requirements
|
||||
// - Faction requirements
|
||||
// - Previous quest requirements
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetQuestByPositionID returns a quest by its position in the journal
|
||||
func (p *Player) GetQuestByPositionID(listPositionID int32) *quests.Quest {
|
||||
// TODO: Implement quest position tracking
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendQuestRequiredSpawns sends spawn updates for quest requirements
|
||||
func (p *Player) SendQuestRequiredSpawns(questID int32) {
|
||||
// TODO: Send spawn visual updates for quest requirements
|
||||
}
|
||||
|
||||
// SendHistoryRequiredSpawns sends spawn updates for history requirements
|
||||
func (p *Player) SendHistoryRequiredSpawns(eventID int32) {
|
||||
// TODO: Send spawn visual updates for history events
|
||||
}
|
||||
|
||||
// SendQuest sends quest data to client
|
||||
func (p *Player) SendQuest(questID int32) {
|
||||
// TODO: Send quest journal packet
|
||||
}
|
||||
|
||||
// UpdateQuestCompleteCount updates quest completion count
|
||||
func (p *Player) UpdateQuestCompleteCount(questID int32) {
|
||||
quest := p.GetCompletedQuest(questID)
|
||||
if quest != nil {
|
||||
IncrementQuestCompleteCount(quest)
|
||||
// TODO: Save to database
|
||||
}
|
||||
}
|
||||
|
||||
// PendingQuestAcceptance handles pending quest rewards
|
||||
func (p *Player) PendingQuestAcceptance(questID, itemID int32, questExists *bool) *quests.Quest {
|
||||
// TODO: Handle quest reward acceptance
|
||||
return nil
|
||||
}
|
||||
|
||||
// AcceptQuestReward accepts a quest reward
|
||||
func (p *Player) AcceptQuestReward(itemID, selectableItemID int32) bool {
|
||||
// TODO: Give quest rewards to player
|
||||
return false
|
||||
}
|
||||
|
||||
// SendQuestStepUpdate sends a quest step update
|
||||
func (p *Player) SendQuestStepUpdate(questID, questStepID int32, displayQuestHelper bool) bool {
|
||||
// TODO: Send quest step update packet
|
||||
return false
|
||||
}
|
||||
|
||||
// GetQuestTemporaryRewards gets temporary quest rewards
|
||||
func (p *Player) GetQuestTemporaryRewards(questID int32, items *[]*Item) {
|
||||
// TODO: Get temporary quest rewards
|
||||
}
|
||||
|
||||
// AddQuestTemporaryReward adds a temporary quest reward
|
||||
func (p *Player) AddQuestTemporaryReward(questID, itemID int32, itemCount int16) {
|
||||
// TODO: Add temporary quest reward
|
||||
}
|
||||
|
||||
// UpdateQuestReward updates quest reward data
|
||||
func (p *Player) UpdateQuestReward(questID int32, qrd *quests.QuestRewards) bool {
|
||||
// TODO: Update quest reward
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckQuestsChatUpdate checks quests for chat updates
|
||||
func (p *Player) CheckQuestsChatUpdate(spawn *spawn.Spawn) []*quests.Quest {
|
||||
// TODO: Check if spawn chat updates any quests
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckQuestsItemUpdate checks quests for item updates
|
||||
func (p *Player) CheckQuestsItemUpdate(item *Item) []*quests.Quest {
|
||||
// TODO: Check if item updates any quests
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckQuestsLocationUpdate checks quests for location updates
|
||||
func (p *Player) CheckQuestsLocationUpdate() []*quests.Quest {
|
||||
// TODO: Check if current location updates any quests
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckQuestsKillUpdate checks quests for kill updates
|
||||
func (p *Player) CheckQuestsKillUpdate(spawn *spawn.Spawn, update bool) []*quests.Quest {
|
||||
// TODO: Check if killing spawn updates any quests
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasQuestUpdateRequirement checks if spawn has quest update requirements
|
||||
func (p *Player) HasQuestUpdateRequirement(spawn *spawn.Spawn) bool {
|
||||
// TODO: Check if spawn updates any active quests
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckQuestsSpellUpdate checks quests for spell updates
|
||||
func (p *Player) CheckQuestsSpellUpdate(spell *spells.Spell) []*quests.Quest {
|
||||
// TODO: Check if spell updates any quests
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckQuestsCraftUpdate checks quests for crafting updates
|
||||
func (p *Player) CheckQuestsCraftUpdate(item *Item, qty int32) {
|
||||
// TODO: Check if crafting updates any quests
|
||||
}
|
||||
|
||||
// CheckQuestsHarvestUpdate checks quests for harvest updates
|
||||
func (p *Player) CheckQuestsHarvestUpdate(item *Item, qty int32) {
|
||||
// TODO: Check if harvesting updates any quests
|
||||
}
|
||||
|
||||
// CheckQuestsFailures checks for quest failures
|
||||
func (p *Player) CheckQuestsFailures() []*quests.Quest {
|
||||
// TODO: Check if any quests have failed
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckQuestRemoveFlag checks if spawn should have quest flag removed
|
||||
func (p *Player) CheckQuestRemoveFlag(spawn *spawn.Spawn) bool {
|
||||
// TODO: Check if quest flag should be removed from spawn
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckQuestFlag returns the quest flag for a spawn
|
||||
func (p *Player) CheckQuestFlag(spawn *spawn.Spawn) int8 {
|
||||
// TODO: Determine quest flag for spawn
|
||||
// 0 = no flag
|
||||
// 1 = quest giver
|
||||
// 2 = quest update
|
||||
// etc.
|
||||
return 0
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"eq2emu/internal/skills"
|
||||
)
|
||||
|
||||
// GetSkillByName returns a skill by name
|
||||
func (p *Player) GetSkillByName(name string, checkUpdate bool) *skills.Skill {
|
||||
return p.GetSkillByNameHelper(name, checkUpdate)
|
||||
}
|
||||
|
||||
// GetSkillByID returns a skill by ID
|
||||
func (p *Player) GetSkillByID(skillID int32, checkUpdate bool) *skills.Skill {
|
||||
// TODO: Implement GetSkillByID when available in skills package
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSkill adds a skill to the player
|
||||
func (p *Player) AddSkill(skillID int32, currentVal, maxVal int16, saveNeeded bool) {
|
||||
p.AddSkillHelper(skillID, currentVal, maxVal, saveNeeded)
|
||||
}
|
||||
|
||||
// RemovePlayerSkill removes a skill from the player
|
||||
func (p *Player) RemovePlayerSkill(skillID int32, save bool) {
|
||||
p.RemoveSkillHelper(skillID)
|
||||
if save {
|
||||
// TODO: Remove from database
|
||||
// TODO: Implement RemoveSkillFromDB when available
|
||||
// p.RemoveSkillFromDB(p.GetSkillByID(skillID, false), save)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveSkillFromDB removes a skill from the database
|
||||
func (p *Player) RemoveSkillFromDB(skill *skills.Skill, save bool) {
|
||||
if skill == nil {
|
||||
return
|
||||
}
|
||||
// TODO: Remove skill from database
|
||||
}
|
||||
|
||||
// AddSkillBonus adds a skill bonus from a spell
|
||||
func (p *Player) AddSkillBonus(spellID, skillID int32, value float32) {
|
||||
// Check if we already have this bonus
|
||||
bonus := p.GetSkillBonus(spellID)
|
||||
if bonus != nil {
|
||||
// Update existing bonus
|
||||
bonus.SkillID = skillID
|
||||
bonus.Value = value
|
||||
} else {
|
||||
// Add new bonus
|
||||
bonus = &SkillBonus{
|
||||
SpellID: spellID,
|
||||
SkillID: skillID,
|
||||
Value: value,
|
||||
}
|
||||
// TODO: Add to skill bonus list
|
||||
}
|
||||
|
||||
// Apply the bonus to the skill
|
||||
skill := p.GetSkillByID(skillID, false)
|
||||
if skill != nil {
|
||||
// TODO: Apply bonus to skill value
|
||||
}
|
||||
}
|
||||
|
||||
// GetSkillBonus returns a skill bonus by spell ID
|
||||
func (p *Player) GetSkillBonus(spellID int32) *SkillBonus {
|
||||
// TODO: Look up skill bonus by spell ID
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSkillBonus removes a skill bonus
|
||||
func (p *Player) RemoveSkillBonus(spellID int32) {
|
||||
bonus := p.GetSkillBonus(spellID)
|
||||
if bonus == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove the bonus from the skill
|
||||
skill := p.GetSkillByID(bonus.SkillID, false)
|
||||
if skill != nil {
|
||||
// TODO: Remove bonus from skill value
|
||||
}
|
||||
|
||||
// TODO: Remove from skill bonus list
|
||||
}
|
@ -1,386 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/spawn"
|
||||
)
|
||||
|
||||
// WasSentSpawn checks if a spawn was already sent to the player
|
||||
func (p *Player) WasSentSpawn(spawnID int32) bool {
|
||||
p.spawnMutex.Lock()
|
||||
defer p.spawnMutex.Unlock()
|
||||
|
||||
if state, exists := p.spawnPacketSent[spawnID]; exists {
|
||||
return state == int8(SPAWN_STATE_SENT)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSendingSpawn checks if a spawn is currently being sent
|
||||
func (p *Player) IsSendingSpawn(spawnID int32) bool {
|
||||
p.spawnMutex.Lock()
|
||||
defer p.spawnMutex.Unlock()
|
||||
|
||||
if state, exists := p.spawnPacketSent[spawnID]; exists {
|
||||
return state == int8(SPAWN_STATE_SENDING)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsRemovingSpawn checks if a spawn is being removed
|
||||
func (p *Player) IsRemovingSpawn(spawnID int32) bool {
|
||||
p.spawnMutex.Lock()
|
||||
defer p.spawnMutex.Unlock()
|
||||
|
||||
if state, exists := p.spawnPacketSent[spawnID]; exists {
|
||||
return state == int8(SPAWN_STATE_REMOVING)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SetSpawnSentState sets the spawn state for tracking
|
||||
func (p *Player) SetSpawnSentState(spawn *spawn.Spawn, state SpawnState) bool {
|
||||
if spawn == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
p.spawnMutex.Lock()
|
||||
defer p.spawnMutex.Unlock()
|
||||
|
||||
spawnID := spawn.GetDatabaseID()
|
||||
p.spawnPacketSent[spawnID] = int8(state)
|
||||
|
||||
// Handle state-specific logic
|
||||
switch state {
|
||||
case SPAWN_STATE_SENT_WAIT:
|
||||
if queueState, exists := p.spawnStateList[spawnID]; exists {
|
||||
queueState.SpawnStateTimer = time.Now().Add(500 * time.Millisecond)
|
||||
} else {
|
||||
p.spawnStateList[spawnID] = &SpawnQueueState{
|
||||
SpawnStateTimer: time.Now().Add(500 * time.Millisecond),
|
||||
IndexID: p.GetIndexForSpawn(spawn),
|
||||
}
|
||||
}
|
||||
case SPAWN_STATE_REMOVING_SLEEP:
|
||||
if queueState, exists := p.spawnStateList[spawnID]; exists {
|
||||
queueState.SpawnStateTimer = time.Now().Add(10 * time.Second)
|
||||
} else {
|
||||
p.spawnStateList[spawnID] = &SpawnQueueState{
|
||||
SpawnStateTimer: time.Now().Add(10 * time.Second),
|
||||
IndexID: p.GetIndexForSpawn(spawn),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// CheckSpawnStateQueue checks spawn states and updates as needed
|
||||
func (p *Player) CheckSpawnStateQueue() {
|
||||
p.spawnMutex.Lock()
|
||||
defer p.spawnMutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for spawnID, queueState := range p.spawnStateList {
|
||||
if now.After(queueState.SpawnStateTimer) {
|
||||
if state, exists := p.spawnPacketSent[spawnID]; exists {
|
||||
switch SpawnState(state) {
|
||||
case SPAWN_STATE_SENT_WAIT:
|
||||
p.spawnPacketSent[spawnID] = int8(SPAWN_STATE_SENT)
|
||||
delete(p.spawnStateList, spawnID)
|
||||
case SPAWN_STATE_REMOVING_SLEEP:
|
||||
// TODO: Remove spawn from index
|
||||
p.spawnPacketSent[spawnID] = int8(SPAWN_STATE_REMOVED)
|
||||
delete(p.spawnStateList, spawnID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetSpawnWithPlayerID returns a spawn by player-specific ID
|
||||
func (p *Player) GetSpawnWithPlayerID(id int32) *spawn.Spawn {
|
||||
p.indexMutex.RLock()
|
||||
defer p.indexMutex.RUnlock()
|
||||
|
||||
if spawn, exists := p.playerSpawnIDMap[id]; exists {
|
||||
return spawn
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIDWithPlayerSpawn returns the player-specific ID for a spawn
|
||||
func (p *Player) GetIDWithPlayerSpawn(spawn *spawn.Spawn) int32 {
|
||||
if spawn == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
p.indexMutex.RLock()
|
||||
defer p.indexMutex.RUnlock()
|
||||
|
||||
if id, exists := p.playerSpawnReverseIDMap[spawn]; exists {
|
||||
return id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetNextSpawnIndex returns the next available spawn index
|
||||
func (p *Player) GetNextSpawnIndex(spawn *spawn.Spawn, setLock bool) int16 {
|
||||
if setLock {
|
||||
p.indexMutex.Lock()
|
||||
defer p.indexMutex.Unlock()
|
||||
}
|
||||
|
||||
// Start from current index and find next available
|
||||
for i := p.spawnIndex + 1; i != p.spawnIndex; i++ {
|
||||
if i > 9999 { // Wrap around
|
||||
i = 1
|
||||
}
|
||||
if _, exists := p.playerSpawnIDMap[int32(i)]; !exists {
|
||||
p.spawnIndex = i
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
// If we've looped all the way around, increment and use it anyway
|
||||
p.spawnIndex++
|
||||
if p.spawnIndex > 9999 {
|
||||
p.spawnIndex = 1
|
||||
}
|
||||
return p.spawnIndex
|
||||
}
|
||||
|
||||
// SetSpawnMap adds a spawn to the player's spawn map
|
||||
func (p *Player) SetSpawnMap(spawn *spawn.Spawn) bool {
|
||||
if spawn == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
p.indexMutex.Lock()
|
||||
defer p.indexMutex.Unlock()
|
||||
|
||||
// Check if spawn already has an ID
|
||||
if id, exists := p.playerSpawnReverseIDMap[spawn]; exists && id > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Get next available index
|
||||
index := p.GetNextSpawnIndex(spawn, false)
|
||||
|
||||
// Set bidirectional mapping
|
||||
p.playerSpawnIDMap[int32(index)] = spawn
|
||||
p.playerSpawnReverseIDMap[spawn] = int32(index)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// SetSpawnMapIndex sets a specific index for a spawn
|
||||
func (p *Player) SetSpawnMapIndex(spawn *spawn.Spawn, index int32) {
|
||||
p.indexMutex.Lock()
|
||||
defer p.indexMutex.Unlock()
|
||||
|
||||
p.playerSpawnIDMap[index] = spawn
|
||||
p.playerSpawnReverseIDMap[spawn] = index
|
||||
}
|
||||
|
||||
// SetSpawnMapAndIndex sets spawn in map and returns the index
|
||||
func (p *Player) SetSpawnMapAndIndex(spawn *spawn.Spawn) int16 {
|
||||
if spawn == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
p.indexMutex.Lock()
|
||||
defer p.indexMutex.Unlock()
|
||||
|
||||
// Check if spawn already has an ID
|
||||
if id, exists := p.playerSpawnReverseIDMap[spawn]; exists && id > 0 {
|
||||
return int16(id)
|
||||
}
|
||||
|
||||
// Get next available index
|
||||
index := p.GetNextSpawnIndex(spawn, false)
|
||||
|
||||
// Set bidirectional mapping
|
||||
p.playerSpawnIDMap[int32(index)] = spawn
|
||||
p.playerSpawnReverseIDMap[spawn] = int32(index)
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
// GetSpawnByIndex returns a spawn by its player-specific index
|
||||
func (p *Player) GetSpawnByIndex(index int16) *spawn.Spawn {
|
||||
return p.GetSpawnWithPlayerID(int32(index))
|
||||
}
|
||||
|
||||
// GetIndexForSpawn returns the player-specific index for a spawn
|
||||
func (p *Player) GetIndexForSpawn(spawn *spawn.Spawn) int16 {
|
||||
return int16(p.GetIDWithPlayerSpawn(spawn))
|
||||
}
|
||||
|
||||
// WasSpawnRemoved checks if a spawn was removed
|
||||
func (p *Player) WasSpawnRemoved(spawn *spawn.Spawn) bool {
|
||||
if spawn == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
p.spawnMutex.Lock()
|
||||
defer p.spawnMutex.Unlock()
|
||||
|
||||
spawnID := spawn.GetDatabaseID()
|
||||
if state, exists := p.spawnPacketSent[spawnID]; exists {
|
||||
return state == int8(SPAWN_STATE_REMOVED)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResetSpawnPackets resets spawn packet state for a spawn
|
||||
func (p *Player) ResetSpawnPackets(id int32) {
|
||||
p.spawnMutex.Lock()
|
||||
defer p.spawnMutex.Unlock()
|
||||
|
||||
delete(p.spawnPacketSent, id)
|
||||
delete(p.spawnStateList, id)
|
||||
}
|
||||
|
||||
// RemoveSpawn removes a spawn from the player's view
|
||||
func (p *Player) RemoveSpawn(spawn *spawn.Spawn, deleteSpawn bool) {
|
||||
if spawn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the player index for this spawn
|
||||
index := p.GetIDWithPlayerSpawn(spawn)
|
||||
if index == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove from spawn maps
|
||||
p.indexMutex.Lock()
|
||||
delete(p.playerSpawnIDMap, index)
|
||||
delete(p.playerSpawnReverseIDMap, spawn)
|
||||
p.indexMutex.Unlock()
|
||||
|
||||
// Remove spawn packets
|
||||
spawnID := spawn.GetDatabaseID()
|
||||
p.infoMutex.Lock()
|
||||
delete(p.spawnInfoPacketList, spawnID)
|
||||
p.infoMutex.Unlock()
|
||||
|
||||
p.visMutex.Lock()
|
||||
delete(p.spawnVisPacketList, spawnID)
|
||||
p.visMutex.Unlock()
|
||||
|
||||
p.posMutex.Lock()
|
||||
delete(p.spawnPosPacketList, spawnID)
|
||||
p.posMutex.Unlock()
|
||||
|
||||
// Reset spawn state
|
||||
p.ResetSpawnPackets(spawnID)
|
||||
|
||||
// TODO: Send despawn packet to client
|
||||
|
||||
if deleteSpawn {
|
||||
// TODO: Actually delete the spawn if requested
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldSendSpawn determines if a spawn should be sent to player
|
||||
func (p *Player) ShouldSendSpawn(spawn *spawn.Spawn) bool {
|
||||
if spawn == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't send self
|
||||
if spawn == p.Entity.Spawn {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if already sent
|
||||
if p.WasSentSpawn(spawn.GetDatabaseID()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check distance
|
||||
distance := p.GetDistance(spawn.GetX(), spawn.GetY(), spawn.GetZ(), true)
|
||||
maxDistance := float32(200.0) // TODO: Get from rule system
|
||||
|
||||
if distance > maxDistance {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: Check visibility flags, stealth, etc.
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// SetSpawnDeleteTime sets the time when a spawn should be deleted
|
||||
func (p *Player) SetSpawnDeleteTime(id int32, deleteTime int32) {
|
||||
// TODO: Implement spawn deletion timer
|
||||
}
|
||||
|
||||
// GetSpawnDeleteTime gets the deletion time for a spawn
|
||||
func (p *Player) GetSpawnDeleteTime(id int32) int32 {
|
||||
// TODO: Implement spawn deletion timer
|
||||
return 0
|
||||
}
|
||||
|
||||
// ClearRemovalTimers clears all spawn removal timers
|
||||
func (p *Player) ClearRemovalTimers() {
|
||||
// TODO: Implement spawn deletion timer clearing
|
||||
}
|
||||
|
||||
// ResetSavedSpawns resets all saved spawn data
|
||||
func (p *Player) ResetSavedSpawns() {
|
||||
p.indexMutex.Lock()
|
||||
p.playerSpawnIDMap = make(map[int32]*spawn.Spawn)
|
||||
p.playerSpawnReverseIDMap = make(map[*spawn.Spawn]int32)
|
||||
// Re-add self
|
||||
p.playerSpawnIDMap[1] = p.Entity.Spawn
|
||||
p.playerSpawnReverseIDMap[p.Entity.Spawn] = 1
|
||||
p.indexMutex.Unlock()
|
||||
|
||||
p.spawnMutex.Lock()
|
||||
p.spawnPacketSent = make(map[int32]int8)
|
||||
p.spawnStateList = make(map[int32]*SpawnQueueState)
|
||||
p.spawnMutex.Unlock()
|
||||
|
||||
p.infoMutex.Lock()
|
||||
p.spawnInfoPacketList = make(map[int32]string)
|
||||
p.infoMutex.Unlock()
|
||||
|
||||
p.visMutex.Lock()
|
||||
p.spawnVisPacketList = make(map[int32]string)
|
||||
p.visMutex.Unlock()
|
||||
|
||||
p.posMutex.Lock()
|
||||
p.spawnPosPacketList = make(map[int32]string)
|
||||
p.posMutex.Unlock()
|
||||
}
|
||||
|
||||
// IsSpawnInRangeList checks if a spawn is in the range list
|
||||
func (p *Player) IsSpawnInRangeList(spawnID int32) bool {
|
||||
p.spawnAggroRangeMutex.RLock()
|
||||
defer p.spawnAggroRangeMutex.RUnlock()
|
||||
|
||||
_, exists := p.playerAggroRangeSpawns[spawnID]
|
||||
return exists
|
||||
}
|
||||
|
||||
// SetSpawnInRangeList sets whether a spawn is in range
|
||||
func (p *Player) SetSpawnInRangeList(spawnID int32, inRange bool) {
|
||||
p.spawnAggroRangeMutex.Lock()
|
||||
defer p.spawnAggroRangeMutex.Unlock()
|
||||
|
||||
if inRange {
|
||||
p.playerAggroRangeSpawns[spawnID] = true
|
||||
} else {
|
||||
delete(p.playerAggroRangeSpawns, spawnID)
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessSpawnRangeUpdates processes spawn range updates
|
||||
func (p *Player) ProcessSpawnRangeUpdates() {
|
||||
// TODO: Implement spawn range update processing
|
||||
// This would check all spawns in range and update visibility
|
||||
}
|
@ -1,623 +0,0 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"eq2emu/internal/spells"
|
||||
)
|
||||
|
||||
// AddSpellBookEntry adds a spell to the player's spell book
|
||||
func (p *Player) AddSpellBookEntry(spellID int32, tier int8, slot int32, spellType int32, timer int32, saveNeeded bool) {
|
||||
p.spellsBookMutex.Lock()
|
||||
defer p.spellsBookMutex.Unlock()
|
||||
|
||||
// Check if spell already exists
|
||||
for _, entry := range p.spells {
|
||||
if entry.SpellID == spellID && entry.Tier == tier {
|
||||
// Update existing entry
|
||||
entry.Slot = slot
|
||||
entry.Type = spellType
|
||||
entry.Timer = timer
|
||||
entry.SaveNeeded = saveNeeded
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create new entry
|
||||
entry := &SpellBookEntry{
|
||||
SpellID: spellID,
|
||||
Tier: tier,
|
||||
Slot: slot,
|
||||
Type: spellType,
|
||||
Timer: timer,
|
||||
SaveNeeded: saveNeeded,
|
||||
Player: p,
|
||||
Visible: true,
|
||||
InUse: false,
|
||||
}
|
||||
|
||||
p.spells = append(p.spells, entry)
|
||||
}
|
||||
|
||||
// GetSpellBookSpell returns a spell book entry by spell ID
|
||||
func (p *Player) GetSpellBookSpell(spellID int32) *SpellBookEntry {
|
||||
p.spellsBookMutex.RLock()
|
||||
defer p.spellsBookMutex.RUnlock()
|
||||
|
||||
for _, entry := range p.spells {
|
||||
if entry.SpellID == spellID {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSpellsSaveNeeded returns spells that need saving to database
|
||||
func (p *Player) GetSpellsSaveNeeded() []*SpellBookEntry {
|
||||
p.spellsBookMutex.RLock()
|
||||
defer p.spellsBookMutex.RUnlock()
|
||||
|
||||
var needSave []*SpellBookEntry
|
||||
for _, entry := range p.spells {
|
||||
if entry.SaveNeeded {
|
||||
needSave = append(needSave, entry)
|
||||
}
|
||||
}
|
||||
return needSave
|
||||
}
|
||||
|
||||
// GetFreeSpellBookSlot returns the next free spell book slot for a type
|
||||
func (p *Player) GetFreeSpellBookSlot(spellType int32) int32 {
|
||||
p.spellsBookMutex.RLock()
|
||||
defer p.spellsBookMutex.RUnlock()
|
||||
|
||||
// Find highest slot for this type
|
||||
var maxSlot int32 = -1
|
||||
for _, entry := range p.spells {
|
||||
if entry.Type == spellType && entry.Slot > maxSlot {
|
||||
maxSlot = entry.Slot
|
||||
}
|
||||
}
|
||||
|
||||
return maxSlot + 1
|
||||
}
|
||||
|
||||
// GetSpellBookSpellIDBySkill returns spell IDs for a given skill
|
||||
func (p *Player) GetSpellBookSpellIDBySkill(skillID int32) []int32 {
|
||||
p.spellsBookMutex.RLock()
|
||||
defer p.spellsBookMutex.RUnlock()
|
||||
|
||||
var spellIDs []int32
|
||||
for range p.spells {
|
||||
// TODO: Check if spell matches skill
|
||||
// spell := master_spell_list.GetSpell(entry.SpellID)
|
||||
// if spell != nil && spell.GetSkillID() == skillID {
|
||||
// spellIDs = append(spellIDs, entry.SpellID)
|
||||
// }
|
||||
}
|
||||
return spellIDs
|
||||
}
|
||||
|
||||
// HasSpell checks if player has a spell
|
||||
func (p *Player) HasSpell(spellID int32, tier int8, includeHigherTiers bool, includePossibleScribe bool) bool {
|
||||
p.spellsBookMutex.RLock()
|
||||
defer p.spellsBookMutex.RUnlock()
|
||||
|
||||
for _, entry := range p.spells {
|
||||
if entry.SpellID == spellID {
|
||||
if tier == 127 || entry.Tier == tier { // Changed from 255 to avoid int8 overflow
|
||||
return true
|
||||
}
|
||||
if includeHigherTiers && entry.Tier > tier {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if includePossibleScribe {
|
||||
// TODO: Check if player can scribe this spell
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetSpellTier returns the tier of a spell the player has
|
||||
func (p *Player) GetSpellTier(spellID int32) int8 {
|
||||
p.spellsBookMutex.RLock()
|
||||
defer p.spellsBookMutex.RUnlock()
|
||||
|
||||
var highestTier int8 = 0
|
||||
for _, entry := range p.spells {
|
||||
if entry.SpellID == spellID && entry.Tier > highestTier {
|
||||
highestTier = entry.Tier
|
||||
}
|
||||
}
|
||||
return highestTier
|
||||
}
|
||||
|
||||
// GetSpellSlot returns the slot of a spell
|
||||
func (p *Player) GetSpellSlot(spellID int32) int8 {
|
||||
entry := p.GetSpellBookSpell(spellID)
|
||||
if entry != nil {
|
||||
return int8(entry.Slot)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// SetSpellStatus sets the status of a spell
|
||||
func (p *Player) SetSpellStatus(spell *spells.Spell, status int8) {
|
||||
if spell == nil {
|
||||
return
|
||||
}
|
||||
|
||||
entry := p.GetSpellBookSpell(spell.GetSpellID())
|
||||
if entry != nil {
|
||||
p.AddSpellStatus(entry, int16(status), true, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveSpellStatus removes a status from a spell
|
||||
func (p *Player) RemoveSpellStatus(spell *spells.Spell, status int8) {
|
||||
if spell == nil {
|
||||
return
|
||||
}
|
||||
|
||||
entry := p.GetSpellBookSpell(spell.GetSpellID())
|
||||
if entry != nil {
|
||||
p.RemoveSpellStatusEntry(entry, int16(status), true, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// AddSpellStatus adds a status to a spell entry
|
||||
func (p *Player) AddSpellStatus(spell *SpellBookEntry, value int16, modifyRecast bool, recast int16) {
|
||||
if spell == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.spellsBookMutex.Lock()
|
||||
defer p.spellsBookMutex.Unlock()
|
||||
|
||||
spell.Status |= int8(value)
|
||||
if modifyRecast {
|
||||
spell.Recast = recast
|
||||
spell.RecastAvailable = 0 // TODO: Calculate actual time
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveSpellStatusEntry removes a status from a spell entry
|
||||
func (p *Player) RemoveSpellStatusEntry(spell *SpellBookEntry, value int16, modifyRecast bool, recast int16) {
|
||||
if spell == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.spellsBookMutex.Lock()
|
||||
defer p.spellsBookMutex.Unlock()
|
||||
|
||||
spell.Status &= ^int8(value)
|
||||
if modifyRecast {
|
||||
spell.Recast = recast
|
||||
spell.RecastAvailable = 0
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveSpellBookEntry removes a spell from the spell book
|
||||
func (p *Player) RemoveSpellBookEntry(spellID int32, removePassivesFromList bool) {
|
||||
p.spellsBookMutex.Lock()
|
||||
defer p.spellsBookMutex.Unlock()
|
||||
|
||||
for i, entry := range p.spells {
|
||||
if entry.SpellID == spellID {
|
||||
// Remove from slice
|
||||
p.spells = append(p.spells[:i], p.spells[i+1:]...)
|
||||
|
||||
if removePassivesFromList {
|
||||
// TODO: Remove from passive list
|
||||
p.RemovePassive(spellID, entry.Tier, true)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteSpellBook deletes spells from the spell book based on type
|
||||
func (p *Player) DeleteSpellBook(typeSelection int8) {
|
||||
p.spellsBookMutex.Lock()
|
||||
defer p.spellsBookMutex.Unlock()
|
||||
|
||||
var keep []*SpellBookEntry
|
||||
for _, entry := range p.spells {
|
||||
deleteIt := false
|
||||
|
||||
// Check type flags
|
||||
if typeSelection&DELETE_TRADESKILLS != 0 {
|
||||
// TODO: Check if tradeskill spell
|
||||
}
|
||||
if typeSelection&DELETE_SPELLS != 0 {
|
||||
// TODO: Check if spell
|
||||
}
|
||||
if typeSelection&DELETE_COMBAT_ART != 0 {
|
||||
// TODO: Check if combat art
|
||||
}
|
||||
if typeSelection&DELETE_ABILITY != 0 {
|
||||
// TODO: Check if ability
|
||||
}
|
||||
if typeSelection&DELETE_NOT_SHOWN != 0 && !entry.Visible {
|
||||
deleteIt = true
|
||||
}
|
||||
|
||||
if !deleteIt {
|
||||
keep = append(keep, entry)
|
||||
}
|
||||
}
|
||||
|
||||
p.spells = keep
|
||||
}
|
||||
|
||||
// ResortSpellBook resorts the spell book
|
||||
func (p *Player) ResortSpellBook(sortBy, order, pattern, maxlvlOnly, bookType int32) {
|
||||
p.spellsBookMutex.Lock()
|
||||
defer p.spellsBookMutex.Unlock()
|
||||
|
||||
// Filter spells based on criteria
|
||||
var filtered []*SpellBookEntry
|
||||
for _, entry := range p.spells {
|
||||
// TODO: Apply filters based on pattern, maxlvlOnly, bookType
|
||||
filtered = append(filtered, entry)
|
||||
}
|
||||
|
||||
// Sort based on sortBy and order
|
||||
switch sortBy {
|
||||
case 0: // By name
|
||||
if order == 0 {
|
||||
sort.Slice(filtered, func(i, j int) bool {
|
||||
return SortSpellEntryByName(filtered[i], filtered[j])
|
||||
})
|
||||
} else {
|
||||
sort.Slice(filtered, func(i, j int) bool {
|
||||
return SortSpellEntryByNameReverse(filtered[i], filtered[j])
|
||||
})
|
||||
}
|
||||
case 1: // By level
|
||||
if order == 0 {
|
||||
sort.Slice(filtered, func(i, j int) bool {
|
||||
return SortSpellEntryByLevel(filtered[i], filtered[j])
|
||||
})
|
||||
} else {
|
||||
sort.Slice(filtered, func(i, j int) bool {
|
||||
return SortSpellEntryByLevelReverse(filtered[i], filtered[j])
|
||||
})
|
||||
}
|
||||
case 2: // By category
|
||||
if order == 0 {
|
||||
sort.Slice(filtered, func(i, j int) bool {
|
||||
return SortSpellEntryByCategory(filtered[i], filtered[j])
|
||||
})
|
||||
} else {
|
||||
sort.Slice(filtered, func(i, j int) bool {
|
||||
return SortSpellEntryByCategoryReverse(filtered[i], filtered[j])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Reassign slots
|
||||
for i, entry := range filtered {
|
||||
entry.Slot = int32(i)
|
||||
}
|
||||
}
|
||||
|
||||
// Spell sorting functions
|
||||
func SortSpellEntryByName(s1, s2 *SpellBookEntry) bool {
|
||||
// TODO: Get spell names and compare
|
||||
return s1.SpellID < s2.SpellID
|
||||
}
|
||||
|
||||
func SortSpellEntryByNameReverse(s1, s2 *SpellBookEntry) bool {
|
||||
return !SortSpellEntryByName(s1, s2)
|
||||
}
|
||||
|
||||
func SortSpellEntryByLevel(s1, s2 *SpellBookEntry) bool {
|
||||
// TODO: Get spell levels and compare
|
||||
return s1.Tier < s2.Tier
|
||||
}
|
||||
|
||||
func SortSpellEntryByLevelReverse(s1, s2 *SpellBookEntry) bool {
|
||||
return !SortSpellEntryByLevel(s1, s2)
|
||||
}
|
||||
|
||||
func SortSpellEntryByCategory(s1, s2 *SpellBookEntry) bool {
|
||||
// TODO: Get spell categories and compare
|
||||
return s1.Type < s2.Type
|
||||
}
|
||||
|
||||
func SortSpellEntryByCategoryReverse(s1, s2 *SpellBookEntry) bool {
|
||||
return !SortSpellEntryByCategory(s1, s2)
|
||||
}
|
||||
|
||||
// LockAllSpells locks all non-tradeskill spells
|
||||
func (p *Player) LockAllSpells() {
|
||||
p.spellsBookMutex.Lock()
|
||||
defer p.spellsBookMutex.Unlock()
|
||||
|
||||
p.allSpellsLocked = true
|
||||
|
||||
for _, entry := range p.spells {
|
||||
// TODO: Check if not tradeskill spell
|
||||
entry.Status |= SPELL_STATUS_LOCK
|
||||
}
|
||||
}
|
||||
|
||||
// UnlockAllSpells unlocks all non-tradeskill spells
|
||||
func (p *Player) UnlockAllSpells(modifyRecast bool, exception *spells.Spell) {
|
||||
p.spellsBookMutex.Lock()
|
||||
defer p.spellsBookMutex.Unlock()
|
||||
|
||||
p.allSpellsLocked = false
|
||||
|
||||
exceptionID := int32(0)
|
||||
if exception != nil {
|
||||
exceptionID = exception.GetSpellID()
|
||||
}
|
||||
|
||||
for _, entry := range p.spells {
|
||||
if entry.SpellID != exceptionID {
|
||||
// TODO: Check if not tradeskill spell
|
||||
entry.Status &= ^SPELL_STATUS_LOCK
|
||||
if modifyRecast {
|
||||
entry.RecastAvailable = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LockSpell locks a spell and all linked spells
|
||||
func (p *Player) LockSpell(spell *spells.Spell, recast int16) {
|
||||
if spell == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Lock the main spell
|
||||
entry := p.GetSpellBookSpell(spell.GetSpellID())
|
||||
if entry != nil {
|
||||
p.AddSpellStatus(entry, SPELL_STATUS_LOCK, true, recast)
|
||||
}
|
||||
|
||||
// TODO: Lock all spells with shared timer
|
||||
}
|
||||
|
||||
// UnlockSpell unlocks a spell and all linked spells
|
||||
func (p *Player) UnlockSpell(spell *spells.Spell) {
|
||||
if spell == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.UnlockSpellByID(spell.GetSpellID(), GetSpellLinkedTimerID(spell.GetSpellData()))
|
||||
}
|
||||
|
||||
// UnlockSpellByID unlocks a spell by ID
|
||||
func (p *Player) UnlockSpellByID(spellID, linkedTimerID int32) {
|
||||
// Unlock the main spell
|
||||
entry := p.GetSpellBookSpell(spellID)
|
||||
if entry != nil {
|
||||
p.RemoveSpellStatusEntry(entry, SPELL_STATUS_LOCK, true, 0)
|
||||
}
|
||||
|
||||
// TODO: Unlock all spells with shared timer
|
||||
if linkedTimerID > 0 {
|
||||
// Get all spells with this timer and unlock them
|
||||
}
|
||||
}
|
||||
|
||||
// LockTSSpells locks tradeskill spells and unlocks combat spells
|
||||
func (p *Player) LockTSSpells() {
|
||||
p.spellsBookMutex.Lock()
|
||||
defer p.spellsBookMutex.Unlock()
|
||||
|
||||
for range p.spells {
|
||||
// TODO: Check if tradeskill spell
|
||||
// if spell.IsTradeskill() {
|
||||
// entry.Status |= SPELL_STATUS_LOCK
|
||||
// } else {
|
||||
// entry.Status &= ^SPELL_STATUS_LOCK
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// UnlockTSSpells unlocks tradeskill spells and locks combat spells
|
||||
func (p *Player) UnlockTSSpells() {
|
||||
p.spellsBookMutex.Lock()
|
||||
defer p.spellsBookMutex.Unlock()
|
||||
|
||||
for range p.spells {
|
||||
// TODO: Check if tradeskill spell
|
||||
// if spell.IsTradeskill() {
|
||||
// entry.Status &= ^SPELL_STATUS_LOCK
|
||||
// } else {
|
||||
// entry.Status |= SPELL_STATUS_LOCK
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// QueueSpell queues a spell for casting
|
||||
func (p *Player) QueueSpell(spell *spells.Spell) {
|
||||
if spell == nil {
|
||||
return
|
||||
}
|
||||
|
||||
entry := p.GetSpellBookSpell(spell.GetSpellID())
|
||||
if entry != nil {
|
||||
p.AddSpellStatus(entry, SPELL_STATUS_QUEUE, false, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// UnQueueSpell removes a spell from the queue
|
||||
func (p *Player) UnQueueSpell(spell *spells.Spell) {
|
||||
if spell == nil {
|
||||
return
|
||||
}
|
||||
|
||||
entry := p.GetSpellBookSpell(spell.GetSpellID())
|
||||
if entry != nil {
|
||||
p.RemoveSpellStatusEntry(entry, SPELL_STATUS_QUEUE, false, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSpellBookSpellsByTimer returns all spells with a given timer
|
||||
func (p *Player) GetSpellBookSpellsByTimer(spell *spells.Spell, timerID int32) []*spells.Spell {
|
||||
var timerSpells []*spells.Spell
|
||||
|
||||
p.spellsBookMutex.RLock()
|
||||
defer p.spellsBookMutex.RUnlock()
|
||||
|
||||
// TODO: Find all spells with matching timer
|
||||
// for _, entry := range p.spells {
|
||||
// spell := master_spell_list.GetSpell(entry.SpellID)
|
||||
// if spell != nil && spell.GetTimerID() == timerID {
|
||||
// timerSpells = append(timerSpells, spell)
|
||||
// }
|
||||
// }
|
||||
|
||||
return timerSpells
|
||||
}
|
||||
|
||||
// AddPassiveSpell adds a passive spell
|
||||
func (p *Player) AddPassiveSpell(id int32, tier int8) {
|
||||
for _, spellID := range p.passiveSpells {
|
||||
if spellID == id {
|
||||
return // Already have it
|
||||
}
|
||||
}
|
||||
p.passiveSpells = append(p.passiveSpells, id)
|
||||
}
|
||||
|
||||
// RemovePassive removes a passive spell
|
||||
func (p *Player) RemovePassive(id int32, tier int8, removeFromList bool) {
|
||||
// TODO: Remove passive effects
|
||||
|
||||
if removeFromList {
|
||||
for i, spellID := range p.passiveSpells {
|
||||
if spellID == id {
|
||||
p.passiveSpells = append(p.passiveSpells[:i], p.passiveSpells[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyPassiveSpells applies all passive spells
|
||||
func (p *Player) ApplyPassiveSpells() {
|
||||
// TODO: Cast all passive spells
|
||||
for range p.passiveSpells {
|
||||
// TODO: Get spell and cast it
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAllPassives removes all passive spell effects
|
||||
func (p *Player) RemoveAllPassives() {
|
||||
// TODO: Remove all passive effects
|
||||
p.passiveSpells = nil
|
||||
}
|
||||
|
||||
// GetSpellSlotMappingCount returns the number of spell slots
|
||||
func (p *Player) GetSpellSlotMappingCount() int16 {
|
||||
p.spellsBookMutex.RLock()
|
||||
defer p.spellsBookMutex.RUnlock()
|
||||
|
||||
return int16(len(p.spells))
|
||||
}
|
||||
|
||||
// GetSpellPacketCount returns the spell packet count
|
||||
func (p *Player) GetSpellPacketCount() int16 {
|
||||
return p.spellCount
|
||||
}
|
||||
|
||||
// AddMaintainedSpell adds a maintained spell effect
|
||||
func (p *Player) AddMaintainedSpell(luaSpell *spells.LuaSpell) {
|
||||
// TODO: Add to maintained effects
|
||||
}
|
||||
|
||||
// RemoveMaintainedSpell removes a maintained spell effect
|
||||
func (p *Player) RemoveMaintainedSpell(luaSpell *spells.LuaSpell) {
|
||||
// TODO: Remove from maintained effects
|
||||
}
|
||||
|
||||
// AddSpellEffect adds a spell effect
|
||||
func (p *Player) AddSpellEffect(luaSpell *spells.LuaSpell, overrideExpireTime int32) {
|
||||
// TODO: Add spell effect
|
||||
}
|
||||
|
||||
// RemoveSpellEffect removes a spell effect
|
||||
func (p *Player) RemoveSpellEffect(luaSpell *spells.LuaSpell) {
|
||||
// TODO: Remove spell effect
|
||||
}
|
||||
|
||||
// GetFreeMaintainedSpellSlot returns a free maintained spell slot
|
||||
func (p *Player) GetFreeMaintainedSpellSlot() *spells.MaintainedEffects {
|
||||
// TODO: Find free slot in maintained effects
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMaintainedSpell returns a maintained spell by ID
|
||||
func (p *Player) GetMaintainedSpell(id int32, onCharLoad bool) *spells.MaintainedEffects {
|
||||
// TODO: Find maintained spell
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMaintainedSpellBySlot returns a maintained spell by slot
|
||||
func (p *Player) GetMaintainedSpellBySlot(slot int8) *spells.MaintainedEffects {
|
||||
// TODO: Find maintained spell by slot
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMaintainedSpells returns all maintained spells
|
||||
func (p *Player) GetMaintainedSpells() *spells.MaintainedEffects {
|
||||
// TODO: Return maintained effects array
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFreeSpellEffectSlot returns a free spell effect slot
|
||||
func (p *Player) GetFreeSpellEffectSlot() *spells.SpellEffects {
|
||||
// TODO: Find free slot in spell effects
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSpellEffects returns all spell effects
|
||||
func (p *Player) GetSpellEffects() *spells.SpellEffects {
|
||||
// TODO: Return spell effects array
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveSpellEffects saves spell effects to database
|
||||
func (p *Player) SaveSpellEffects() {
|
||||
if p.stopSaveSpellEffects {
|
||||
return
|
||||
}
|
||||
// TODO: Save spell effects to database
|
||||
}
|
||||
|
||||
// GetTierUp returns the next tier for a given tier
|
||||
func (p *Player) GetTierUp(tier int16) int16 {
|
||||
switch tier {
|
||||
case 0:
|
||||
return 1
|
||||
case 1:
|
||||
return 2
|
||||
case 2:
|
||||
return 3
|
||||
case 3:
|
||||
return 4
|
||||
case 4:
|
||||
return 5
|
||||
case 5:
|
||||
return 6
|
||||
case 6:
|
||||
return 7
|
||||
case 7:
|
||||
return 8
|
||||
case 8:
|
||||
return 9
|
||||
case 9:
|
||||
return 10
|
||||
default:
|
||||
return tier + 1
|
||||
}
|
||||
}
|
@ -1,529 +0,0 @@
|
||||
package player
|
||||
|
||||
// This file contains placeholder stub methods to make the player package compile
|
||||
// These methods should be properly implemented when the corresponding systems are ready
|
||||
|
||||
import (
|
||||
"eq2emu/internal/entity"
|
||||
"eq2emu/internal/quests"
|
||||
"eq2emu/internal/skills"
|
||||
"eq2emu/internal/spells"
|
||||
)
|
||||
|
||||
// Player-level flags handling since InfoStruct doesn't have these methods yet
|
||||
var playerFlags = make(map[int32]int32)
|
||||
var playerFlags2 = make(map[int32]int32)
|
||||
|
||||
// GetPlayerFlags returns player flags for a character
|
||||
func (p *Player) GetPlayerFlags() int32 {
|
||||
return playerFlags[p.GetCharacterID()]
|
||||
}
|
||||
|
||||
// SetPlayerFlags sets player flags for a character
|
||||
func (p *Player) SetPlayerFlags(flags int32) {
|
||||
playerFlags[p.GetCharacterID()] = flags
|
||||
}
|
||||
|
||||
// GetPlayerFlags2 returns player flags2 for a character
|
||||
func (p *Player) GetPlayerFlags2() int32 {
|
||||
return playerFlags2[p.GetCharacterID()]
|
||||
}
|
||||
|
||||
// SetPlayerFlags2 sets player flags2 for a character
|
||||
func (p *Player) SetPlayerFlags2(flags int32) {
|
||||
playerFlags2[p.GetCharacterID()] = flags
|
||||
}
|
||||
|
||||
// Player stub methods that may be called by other packages
|
||||
|
||||
// GetPrimaryStat returns the primary stat for the player's class (stub)
|
||||
func (p *Player) GetPrimaryStat() int32 {
|
||||
// TODO: Calculate based on class
|
||||
return 100
|
||||
}
|
||||
|
||||
// GetTarget returns the player's current target (stub)
|
||||
func (p *Player) GetTarget() any {
|
||||
// TODO: Implement targeting system
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsStunned returns whether the player is stunned (stub)
|
||||
func (p *Player) IsStunned() bool {
|
||||
// TODO: Implement status effect system
|
||||
return false
|
||||
}
|
||||
|
||||
// IsMezzed returns whether the player is mezzed (stub)
|
||||
func (p *Player) IsMezzed() bool {
|
||||
// TODO: Implement status effect system
|
||||
return false
|
||||
}
|
||||
|
||||
// Player-level combat state handling
|
||||
var playerEngageCommands = make(map[int32]int32)
|
||||
|
||||
// GetPlayerEngageCommands returns combat state for a character
|
||||
func (p *Player) GetPlayerEngageCommands() int32 {
|
||||
return playerEngageCommands[p.GetCharacterID()]
|
||||
}
|
||||
|
||||
// SetPlayerEngageCommands sets combat state for a character
|
||||
func (p *Player) SetPlayerEngageCommands(commands int32) {
|
||||
playerEngageCommands[p.GetCharacterID()] = commands
|
||||
}
|
||||
|
||||
// IsDead returns whether the player is dead (stub)
|
||||
func (p *Player) IsDead() bool {
|
||||
// TODO: Implement death state tracking
|
||||
return p.GetHP() <= 0
|
||||
}
|
||||
|
||||
// Note: IsPlayer method is already implemented in player.go, so removed duplicate
|
||||
|
||||
// Entity stub methods for combat target
|
||||
var entityDeathState = make(map[*entity.Entity]bool)
|
||||
|
||||
// IsDead checks if an entity is dead (stub for Entity type)
|
||||
func IsDead(e *entity.Entity) bool {
|
||||
// TODO: Implement proper entity death checking
|
||||
return entityDeathState[e]
|
||||
}
|
||||
|
||||
// Coin management stubs
|
||||
var playerCoins = make(map[int32]int64) // characterID -> coin amount
|
||||
|
||||
// AddCoin adds coins to the player's InfoStruct (stub)
|
||||
func (p *Player) AddCoin(amount int64) {
|
||||
current := playerCoins[p.GetCharacterID()]
|
||||
playerCoins[p.GetCharacterID()] = current + amount
|
||||
}
|
||||
|
||||
// GetCoin returns the player's current coin amount (stub)
|
||||
func (p *Player) GetCoin() int64 {
|
||||
return playerCoins[p.GetCharacterID()]
|
||||
}
|
||||
|
||||
// SubtractCoin removes coins from the player (stub)
|
||||
func (p *Player) SubtractCoin(amount int64) bool {
|
||||
current := playerCoins[p.GetCharacterID()]
|
||||
if current >= amount {
|
||||
playerCoins[p.GetCharacterID()] = current - amount
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InfoStruct coin access stubs - these methods don't exist on InfoStruct yet
|
||||
var playerCoinBreakdown = make(map[int32]map[string]int32) // characterID -> coin type -> amount
|
||||
|
||||
// GetCoinCopper returns copper coins (stub)
|
||||
func (p *Player) GetInfoStructCoinCopper() int32 {
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
return 0
|
||||
}
|
||||
return playerCoinBreakdown[p.GetCharacterID()]["copper"]
|
||||
}
|
||||
|
||||
// GetCoinSilver returns silver coins (stub)
|
||||
func (p *Player) GetInfoStructCoinSilver() int32 {
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
return 0
|
||||
}
|
||||
return playerCoinBreakdown[p.GetCharacterID()]["silver"]
|
||||
}
|
||||
|
||||
// GetCoinGold returns gold coins (stub)
|
||||
func (p *Player) GetInfoStructCoinGold() int32 {
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
return 0
|
||||
}
|
||||
return playerCoinBreakdown[p.GetCharacterID()]["gold"]
|
||||
}
|
||||
|
||||
// GetCoinPlat returns platinum coins (stub)
|
||||
func (p *Player) GetInfoStructCoinPlat() int32 {
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
return 0
|
||||
}
|
||||
return playerCoinBreakdown[p.GetCharacterID()]["plat"]
|
||||
}
|
||||
|
||||
// Bank coin methods (stubs)
|
||||
func (p *Player) GetInfoStructBankCoinCopper() int32 {
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
return 0
|
||||
}
|
||||
return playerCoinBreakdown[p.GetCharacterID()]["bank_copper"]
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructBankCoinSilver() int32 {
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
return 0
|
||||
}
|
||||
return playerCoinBreakdown[p.GetCharacterID()]["bank_silver"]
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructBankCoinGold() int32 {
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
return 0
|
||||
}
|
||||
return playerCoinBreakdown[p.GetCharacterID()]["bank_gold"]
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructBankCoinPlat() int32 {
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
return 0
|
||||
}
|
||||
return playerCoinBreakdown[p.GetCharacterID()]["bank_plat"]
|
||||
}
|
||||
|
||||
// GetStatusPoints returns status points (stub)
|
||||
func (p *Player) GetInfoStructStatusPoints() int32 {
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
return 0
|
||||
}
|
||||
return playerCoinBreakdown[p.GetCharacterID()]["status"]
|
||||
}
|
||||
|
||||
// Player methods that don't exist on Entity/Spawn yet (stubs)
|
||||
func (p *Player) SetRace(race int8) {
|
||||
// TODO: Implement race setting on entity/spawn
|
||||
// For now, store in a map
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
playerCoinBreakdown[p.GetCharacterID()] = make(map[string]int32)
|
||||
}
|
||||
playerCoinBreakdown[p.GetCharacterID()]["race"] = int32(race)
|
||||
}
|
||||
|
||||
func (p *Player) GetRace() int8 {
|
||||
// TODO: Implement race getting from entity/spawn
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
return 0
|
||||
}
|
||||
return int8(playerCoinBreakdown[p.GetCharacterID()]["race"])
|
||||
}
|
||||
|
||||
func (p *Player) SetZone(zoneID int32) {
|
||||
// TODO: Implement zone setting on entity/spawn
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
playerCoinBreakdown[p.GetCharacterID()] = make(map[string]int32)
|
||||
}
|
||||
playerCoinBreakdown[p.GetCharacterID()]["zone"] = zoneID
|
||||
}
|
||||
|
||||
func (p *Player) GetZone() int32 {
|
||||
// TODO: Implement zone getting from entity/spawn
|
||||
if playerCoinBreakdown[p.GetCharacterID()] == nil {
|
||||
return 0
|
||||
}
|
||||
return playerCoinBreakdown[p.GetCharacterID()]["zone"]
|
||||
}
|
||||
|
||||
// Experience vitality methods (InfoStruct stubs)
|
||||
func (p *Player) GetInfoStructXPVitality() float32 {
|
||||
// TODO: Implement XP vitality tracking
|
||||
return 100.0 // Default vitality
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructTSXPVitality() float32 {
|
||||
// TODO: Implement tradeskill XP vitality tracking
|
||||
return 100.0 // Default vitality
|
||||
}
|
||||
|
||||
// More InfoStruct experience method stubs
|
||||
var playerXPData = make(map[int32]map[string]float64) // characterID -> xp type -> value
|
||||
|
||||
func (p *Player) GetInfoStructXPDebt() float32 {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
return 0.0
|
||||
}
|
||||
return float32(playerXPData[charID]["xp_debt"])
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructTSXPDebt() float32 {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
return 0.0
|
||||
}
|
||||
return float32(playerXPData[charID]["ts_xp_debt"])
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructXPNeeded(xp float64) {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
playerXPData[charID] = make(map[string]float64)
|
||||
}
|
||||
playerXPData[charID]["xp_needed"] = xp
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructXP(xp float64) {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
playerXPData[charID] = make(map[string]float64)
|
||||
}
|
||||
playerXPData[charID]["xp"] = xp
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructTSXPNeeded(xp float64) {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
playerXPData[charID] = make(map[string]float64)
|
||||
}
|
||||
playerXPData[charID]["ts_xp_needed"] = xp
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructTSXP(xp float64) {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
playerXPData[charID] = make(map[string]float64)
|
||||
}
|
||||
playerXPData[charID]["ts_xp"] = xp
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructXPNeeded() float64 {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
return 1000.0 // Default XP needed
|
||||
}
|
||||
return playerXPData[charID]["xp_needed"]
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructXP() float64 {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
return 0.0
|
||||
}
|
||||
return playerXPData[charID]["xp"]
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructTSXPNeeded() float64 {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
return 1000.0 // Default TS XP needed
|
||||
}
|
||||
return playerXPData[charID]["ts_xp_needed"]
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructTSXP() float64 {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
return 0.0
|
||||
}
|
||||
return playerXPData[charID]["ts_xp"]
|
||||
}
|
||||
|
||||
// XP Debt methods
|
||||
func (p *Player) SetInfoStructXPDebt(debt float32) {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
playerXPData[charID] = make(map[string]float64)
|
||||
}
|
||||
playerXPData[charID]["xp_debt"] = float64(debt)
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructTSXPDebt(debt float32) {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
playerXPData[charID] = make(map[string]float64)
|
||||
}
|
||||
playerXPData[charID]["ts_xp_debt"] = float64(debt)
|
||||
}
|
||||
|
||||
// TS Level methods
|
||||
func (p *Player) GetInfoStructTSLevel() int8 {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
return 1 // Default level
|
||||
}
|
||||
return int8(playerXPData[charID]["ts_level"])
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructTSLevel(level int8) {
|
||||
charID := p.GetCharacterID()
|
||||
if playerXPData[charID] == nil {
|
||||
playerXPData[charID] = make(map[string]float64)
|
||||
}
|
||||
playerXPData[charID]["ts_level"] = float64(level)
|
||||
}
|
||||
|
||||
// Player wrapper methods for TS level
|
||||
func (p *Player) GetTSLevel() int8 {
|
||||
return p.GetInfoStructTSLevel()
|
||||
}
|
||||
|
||||
func (p *Player) SetTSLevel(level int8) {
|
||||
p.SetInfoStructTSLevel(level)
|
||||
p.SetCharSheetChanged(true)
|
||||
}
|
||||
|
||||
// GetSpawnID returns the spawn ID (in EverQuest II, players use same ID as spawn)
|
||||
func (p *Player) GetSpawnID() int32 {
|
||||
// Use the player's spawnID field
|
||||
return p.spawnID
|
||||
}
|
||||
|
||||
// SetSpawnID sets the spawn ID
|
||||
func (p *Player) SetSpawnID(id int32) {
|
||||
p.spawnID = id
|
||||
}
|
||||
|
||||
// AddSecondaryEntityCommand adds a secondary command (stub)
|
||||
func (p *Player) AddSecondaryEntityCommand(name string, distance float32, command, errorText string, castTime int16, spellID int32) {
|
||||
// TODO: Implement secondary entity commands
|
||||
}
|
||||
|
||||
// Position and movement data storage (stubs for appearance system)
|
||||
var playerMovementData = make(map[int32]map[string]float32) // characterID -> movement type -> value
|
||||
|
||||
// SetPos sets positional data (stub)
|
||||
func (p *Player) SetPos(ptr *float32, value float32, updateFlags bool) {
|
||||
// Since we can't access the appearance system, just store the value
|
||||
charID := p.GetCharacterID()
|
||||
if playerMovementData[charID] == nil {
|
||||
playerMovementData[charID] = make(map[string]float32)
|
||||
}
|
||||
*ptr = value // Set the pointer value if it's valid
|
||||
// TODO: Handle updateFlags when packet system is available
|
||||
}
|
||||
|
||||
// GetPos gets positional data (stub)
|
||||
func (p *Player) GetPos(key string) float32 {
|
||||
charID := p.GetCharacterID()
|
||||
if playerMovementData[charID] == nil {
|
||||
return 0.0
|
||||
}
|
||||
return playerMovementData[charID][key]
|
||||
}
|
||||
|
||||
// XP bar display methods (InfoStruct stubs)
|
||||
func (p *Player) SetInfoStructXPYellow(value int16) {
|
||||
// TODO: Implement XP bar display
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructXPBlue(value int16) {
|
||||
// TODO: Implement XP bar display
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructXPBlueVitalityBar(value int16) {
|
||||
// TODO: Implement XP bar display
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructXPYellowVitalityBar(value int16) {
|
||||
// TODO: Implement XP bar display
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructTSXPYellow(value int16) {
|
||||
// TODO: Implement TS XP bar display
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructTSXPBlue(value int16) {
|
||||
// TODO: Implement TS XP bar display
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructTSXPBlueVitalityBar(value int16) {
|
||||
// TODO: Implement TS XP bar display
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructTSXPYellowVitalityBar(value int16) {
|
||||
// TODO: Implement TS XP bar display
|
||||
}
|
||||
|
||||
// XP bar getter methods (InfoStruct stubs)
|
||||
func (p *Player) GetInfoStructXPBlue() int16 {
|
||||
// TODO: Implement XP bar display
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructXPYellow() int16 {
|
||||
// TODO: Implement XP bar display
|
||||
return 0
|
||||
}
|
||||
|
||||
// Tradeskill XP bar methods
|
||||
func (p *Player) SetInfoStructTradeskillExpYellow(value int16) {
|
||||
// TODO: Implement TS XP bar display
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructTradeskillExpBlue(value int16) {
|
||||
// TODO: Implement TS XP bar display
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructTSExpVitalityBlue(value int16) {
|
||||
// TODO: Implement TS XP vitality bar display
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructTSExpVitalityYellow(value int16) {
|
||||
// TODO: Implement TS XP vitality bar display
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructTradeskillExpBlue() int16 {
|
||||
// TODO: Implement TS XP bar display
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p *Player) GetInfoStructTradeskillExpYellow() int16 {
|
||||
// TODO: Implement TS XP bar display
|
||||
return 0
|
||||
}
|
||||
|
||||
// Account age methods (InfoStruct stubs)
|
||||
func (p *Player) SetInfoStructAccountAgeBase(value int32) {
|
||||
// TODO: Implement account age tracking
|
||||
}
|
||||
|
||||
func (p *Player) SetInfoStructAccountAge(value int32) {
|
||||
// TODO: Implement account age tracking
|
||||
}
|
||||
|
||||
// Quest helper functions - working around missing quest methods
|
||||
// These should ideally be proper methods on the Quest type in the quests package
|
||||
|
||||
func GetQuestCompleteCount(q *quests.Quest) int32 {
|
||||
// TODO: Implement quest completion tracking
|
||||
// For now return 0 as a placeholder
|
||||
return 0
|
||||
}
|
||||
|
||||
func GetQuestID(q *quests.Quest) int32 {
|
||||
// TODO: This should access q.ID field when available
|
||||
return 0
|
||||
}
|
||||
|
||||
func GetQuestStep(q *quests.Quest) int16 {
|
||||
// TODO: Implement quest step retrieval
|
||||
return 0
|
||||
}
|
||||
|
||||
func GetQuestTaskGroup(q *quests.Quest) int8 {
|
||||
// TODO: Implement quest task group system
|
||||
return 0
|
||||
}
|
||||
|
||||
func IncrementQuestCompleteCount(q *quests.Quest) {
|
||||
// TODO: Implement quest completion tracking
|
||||
}
|
||||
|
||||
// PlayerSkillList helper methods to work around method signature differences
|
||||
func (p *Player) GetSkillByNameHelper(name string, checkUpdate bool) *skills.Skill {
|
||||
// TODO: Work around method signature mismatch
|
||||
// The actual method only takes name as parameter
|
||||
return p.skillList.GetSkillByName(name)
|
||||
}
|
||||
|
||||
func (p *Player) AddSkillHelper(skillID int32, currentVal, maxVal int16, saveNeeded bool) {
|
||||
// TODO: Create proper Skill object and add it
|
||||
// For now, this is a placeholder stub
|
||||
}
|
||||
|
||||
func (p *Player) RemoveSkillHelper(skillID int32) {
|
||||
// TODO: Remove skill by ID
|
||||
// For now, this is a placeholder stub
|
||||
}
|
||||
|
||||
// SpellData method stubs
|
||||
func GetSpellLinkedTimerID(spellData *spells.SpellData) int32 {
|
||||
// TODO: Implement LinkedTimerID field access
|
||||
return 0
|
||||
}
|
550
internal/player/unified.go
Normal file
550
internal/player/unified.go
Normal file
@ -0,0 +1,550 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// Logger interface for player system
|
||||
type Logger interface {
|
||||
Debug(string, ...interface{})
|
||||
Info(string, ...interface{})
|
||||
Warn(string, ...interface{})
|
||||
Error(string, ...interface{})
|
||||
}
|
||||
|
||||
// Config interface for player system
|
||||
type Config interface {
|
||||
GetString(string) string
|
||||
GetInt(string) int
|
||||
GetBool(string) bool
|
||||
}
|
||||
|
||||
// PlayerManager manages all player operations in a unified system
|
||||
type PlayerManager struct {
|
||||
db *database.Database
|
||||
logger Logger
|
||||
config Config
|
||||
playersLock sync.RWMutex
|
||||
players map[int32]*Player
|
||||
experienceCalc ExperienceCalculator
|
||||
combatSystem CombatSystem
|
||||
questSystem QuestSystem
|
||||
dbPersister DatabasePersister
|
||||
eventManager EventManager
|
||||
}
|
||||
|
||||
// ExperienceCalculator handles all experience calculations
|
||||
type ExperienceCalculator struct {
|
||||
baseLevelXP []int64
|
||||
tsLevelXP []int64
|
||||
vitaeLevels []float32
|
||||
aaMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// CombatSystem handles all combat-related functionality
|
||||
type CombatSystem struct {
|
||||
activeCombats map[int32]*CombatSession
|
||||
combatMutex sync.RWMutex
|
||||
damageTypes map[int32]string
|
||||
}
|
||||
|
||||
// QuestSystem manages all quest-related functionality
|
||||
type QuestSystem struct {
|
||||
questCache map[int32]*Quest
|
||||
questMutex sync.RWMutex
|
||||
completionCache map[int32]bool
|
||||
}
|
||||
|
||||
// DatabasePersister handles all database operations
|
||||
type DatabasePersister struct {
|
||||
db *database.Database
|
||||
saveMutex sync.Mutex
|
||||
saveQueue chan *Player
|
||||
stopChan chan bool
|
||||
}
|
||||
|
||||
// EventManager handles all player events
|
||||
type EventManager struct {
|
||||
eventQueue chan PlayerEvent
|
||||
subscribers map[EventType][]EventHandler
|
||||
eventMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// PlayerEvent represents a player event
|
||||
type PlayerEvent struct {
|
||||
Type EventType
|
||||
PlayerID int32
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
// EventType represents the type of player event
|
||||
type EventType int
|
||||
|
||||
const (
|
||||
EventPlayerLogin EventType = iota
|
||||
EventPlayerLogout
|
||||
EventPlayerLevelUp
|
||||
EventPlayerDeath
|
||||
EventPlayerCombat
|
||||
EventPlayerQuest
|
||||
)
|
||||
|
||||
// EventHandler handles player events
|
||||
type EventHandler func(PlayerEvent)
|
||||
|
||||
// CombatSession represents an active combat session
|
||||
type CombatSession struct {
|
||||
PlayerID int32
|
||||
TargetID int32
|
||||
StartTime time.Time
|
||||
DamageDealt int32
|
||||
DamageTaken int32
|
||||
}
|
||||
|
||||
// Quest represents a quest (placeholder for full implementation)
|
||||
type Quest struct {
|
||||
ID int32
|
||||
Name string
|
||||
Description string
|
||||
Status int32
|
||||
}
|
||||
|
||||
// NewPlayerManager creates a new unified player manager
|
||||
func NewPlayerManager(db *database.Database, logger Logger, config Config) *PlayerManager {
|
||||
pm := &PlayerManager{
|
||||
db: db,
|
||||
logger: logger,
|
||||
config: config,
|
||||
players: make(map[int32]*Player),
|
||||
experienceCalc: ExperienceCalculator{
|
||||
baseLevelXP: make([]int64, 101),
|
||||
tsLevelXP: make([]int64, 101),
|
||||
vitaeLevels: make([]float32, 101),
|
||||
},
|
||||
combatSystem: CombatSystem{
|
||||
activeCombats: make(map[int32]*CombatSession),
|
||||
damageTypes: make(map[int32]string),
|
||||
},
|
||||
questSystem: QuestSystem{
|
||||
questCache: make(map[int32]*Quest),
|
||||
completionCache: make(map[int32]bool),
|
||||
},
|
||||
dbPersister: DatabasePersister{
|
||||
db: db,
|
||||
saveQueue: make(chan *Player, 100),
|
||||
stopChan: make(chan bool),
|
||||
},
|
||||
eventManager: EventManager{
|
||||
eventQueue: make(chan PlayerEvent, 1000),
|
||||
subscribers: make(map[EventType][]EventHandler),
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize experience tables
|
||||
pm.initializeExperienceTables()
|
||||
|
||||
// Start background processors
|
||||
pm.startBackgroundProcessors()
|
||||
|
||||
return pm
|
||||
}
|
||||
|
||||
// AddPlayer adds a player to the manager
|
||||
func (pm *PlayerManager) AddPlayer(player *Player) {
|
||||
pm.playersLock.Lock()
|
||||
defer pm.playersLock.Unlock()
|
||||
|
||||
pm.players[player.charID] = player
|
||||
pm.logger.Info("Player added: %d", player.charID)
|
||||
|
||||
// Send login event
|
||||
pm.eventManager.eventQueue <- PlayerEvent{
|
||||
Type: EventPlayerLogin,
|
||||
PlayerID: player.charID,
|
||||
Data: player,
|
||||
}
|
||||
}
|
||||
|
||||
// RemovePlayer removes a player from the manager
|
||||
func (pm *PlayerManager) RemovePlayer(charID int32) {
|
||||
pm.playersLock.Lock()
|
||||
defer pm.playersLock.Unlock()
|
||||
|
||||
if player, exists := pm.players[charID]; exists {
|
||||
// Send logout event
|
||||
pm.eventManager.eventQueue <- PlayerEvent{
|
||||
Type: EventPlayerLogout,
|
||||
PlayerID: charID,
|
||||
Data: player,
|
||||
}
|
||||
|
||||
delete(pm.players, charID)
|
||||
pm.logger.Info("Player removed: %d", charID)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPlayer retrieves a player by character ID
|
||||
func (pm *PlayerManager) GetPlayer(charID int32) *Player {
|
||||
pm.playersLock.RLock()
|
||||
defer pm.playersLock.RUnlock()
|
||||
return pm.players[charID]
|
||||
}
|
||||
|
||||
// GetAllPlayers returns all active players
|
||||
func (pm *PlayerManager) GetAllPlayers() []*Player {
|
||||
pm.playersLock.RLock()
|
||||
defer pm.playersLock.RUnlock()
|
||||
|
||||
players := make([]*Player, 0, len(pm.players))
|
||||
for _, player := range pm.players {
|
||||
players = append(players, player)
|
||||
}
|
||||
return players
|
||||
}
|
||||
|
||||
// UpdatePlayerLevel handles player level progression
|
||||
func (pm *PlayerManager) UpdatePlayerLevel(charID int32, newLevel int16) error {
|
||||
player := pm.GetPlayer(charID)
|
||||
if player == nil {
|
||||
return fmt.Errorf("player not found: %d", charID)
|
||||
}
|
||||
|
||||
oldLevel := player.GetLevel()
|
||||
player.SetLevel(newLevel)
|
||||
|
||||
// Send level up event
|
||||
pm.eventManager.eventQueue <- PlayerEvent{
|
||||
Type: EventPlayerLevelUp,
|
||||
PlayerID: charID,
|
||||
Data: map[string]interface{}{"oldLevel": oldLevel, "newLevel": newLevel},
|
||||
}
|
||||
|
||||
// Send level up packet
|
||||
pm.sendLevelUpPacket(player)
|
||||
|
||||
pm.logger.Info("Player %d leveled up from %d to %d", charID, oldLevel, newLevel)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddExperience adds experience to a player
|
||||
func (pm *PlayerManager) AddExperience(charID int32, xpAmount int64, xpType string) error {
|
||||
player := pm.GetPlayer(charID)
|
||||
if player == nil {
|
||||
return fmt.Errorf("player not found: %d", charID)
|
||||
}
|
||||
|
||||
// Calculate vitae modifier
|
||||
vitaeModifier := pm.experienceCalc.getVitaeModifier(int16(player.GetLevel()))
|
||||
adjustedXP := int64(float32(xpAmount) * vitaeModifier)
|
||||
|
||||
// Validate XP type
|
||||
switch xpType {
|
||||
case "adventure", "tradeskill":
|
||||
// Valid types - continue
|
||||
default:
|
||||
return fmt.Errorf("unknown XP type: %s", xpType)
|
||||
}
|
||||
|
||||
// For now, just log the XP addition - actual XP modification would require
|
||||
// accessor methods to be added to InfoStruct
|
||||
pm.logger.Debug("Would add %d %s XP to player %d (vitae: %.2f)", adjustedXP, xpType, charID, vitaeModifier)
|
||||
|
||||
// Check for level up (simplified version)
|
||||
pm.checkLevelUp(player, xpType)
|
||||
|
||||
// Send XP update packet
|
||||
pm.sendXPUpdatePacket(player, adjustedXP, xpType)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessCombat handles combat between entities
|
||||
func (pm *PlayerManager) ProcessCombat(attackerID, defenderID int32, damageAmount int32) error {
|
||||
attacker := pm.GetPlayer(attackerID)
|
||||
if attacker == nil {
|
||||
return fmt.Errorf("attacker not found: %d", attackerID)
|
||||
}
|
||||
|
||||
// Create or update combat session
|
||||
pm.combatSystem.combatMutex.Lock()
|
||||
session, exists := pm.combatSystem.activeCombats[attackerID]
|
||||
if !exists {
|
||||
session = &CombatSession{
|
||||
PlayerID: attackerID,
|
||||
TargetID: defenderID,
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
pm.combatSystem.activeCombats[attackerID] = session
|
||||
}
|
||||
session.DamageDealt += damageAmount
|
||||
pm.combatSystem.combatMutex.Unlock()
|
||||
|
||||
// Send combat event
|
||||
pm.eventManager.eventQueue <- PlayerEvent{
|
||||
Type: EventPlayerCombat,
|
||||
PlayerID: attackerID,
|
||||
Data: session,
|
||||
}
|
||||
|
||||
// Send combat packet
|
||||
pm.sendCombatPacket(attacker, defenderID, damageAmount)
|
||||
|
||||
pm.logger.Debug("Combat: Player %d dealt %d damage to %d", attackerID, damageAmount, defenderID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCurrency updates a player's currency
|
||||
func (pm *PlayerManager) UpdateCurrency(charID int32, currencyType string, amount int64) error {
|
||||
player := pm.GetPlayer(charID)
|
||||
if player == nil {
|
||||
return fmt.Errorf("player not found: %d", charID)
|
||||
}
|
||||
|
||||
// Get the player's info struct
|
||||
info := player.GetInfoStruct()
|
||||
if info == nil {
|
||||
return fmt.Errorf("player info not available")
|
||||
}
|
||||
|
||||
// Update currency based on type
|
||||
switch currencyType {
|
||||
case "coin":
|
||||
info.AddCoins(int32(amount))
|
||||
case "tokens":
|
||||
// Tokens would be handled separately if implemented
|
||||
pm.logger.Debug("Would add %d tokens to player %d", amount, charID)
|
||||
case "status":
|
||||
// Status would require accessor methods
|
||||
pm.logger.Debug("Would add %d status to player %d", amount, charID)
|
||||
default:
|
||||
return fmt.Errorf("unknown currency type: %s", currencyType)
|
||||
}
|
||||
|
||||
// Send currency update packet
|
||||
pm.sendCurrencyUpdatePacket(player, currencyType, amount)
|
||||
|
||||
pm.logger.Debug("Updated %s currency for player %d: %d", currencyType, charID, amount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SavePlayer saves a player to the database
|
||||
func (pm *PlayerManager) SavePlayer(charID int32) error {
|
||||
player := pm.GetPlayer(charID)
|
||||
if player == nil {
|
||||
return fmt.Errorf("player not found: %d", charID)
|
||||
}
|
||||
|
||||
// Queue for background save
|
||||
select {
|
||||
case pm.dbPersister.saveQueue <- player:
|
||||
pm.logger.Debug("Queued player %d for save", charID)
|
||||
default:
|
||||
pm.logger.Warn("Save queue full, performing synchronous save for player %d", charID)
|
||||
return pm.savePlayerSync(player)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveAllPlayers saves all active players
|
||||
func (pm *PlayerManager) SaveAllPlayers() error {
|
||||
players := pm.GetAllPlayers()
|
||||
for _, player := range players {
|
||||
if err := pm.SavePlayer(player.charID); err != nil {
|
||||
pm.logger.Error("Failed to save player %d: %v", player.charID, err)
|
||||
}
|
||||
}
|
||||
pm.logger.Info("Initiated save for %d players", len(players))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the player manager
|
||||
func (pm *PlayerManager) Shutdown() {
|
||||
pm.logger.Info("Shutting down player manager...")
|
||||
|
||||
// Stop background processors
|
||||
close(pm.dbPersister.stopChan)
|
||||
|
||||
// Save all players
|
||||
pm.SaveAllPlayers()
|
||||
|
||||
// Wait for save queue to empty
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
pm.logger.Info("Player manager shutdown complete")
|
||||
}
|
||||
|
||||
// initializeExperienceTables initializes the XP lookup tables
|
||||
func (pm *PlayerManager) initializeExperienceTables() {
|
||||
// Initialize base level XP requirements (example values)
|
||||
for i := 0; i < 101; i++ {
|
||||
pm.experienceCalc.baseLevelXP[i] = int64(i * i * 100)
|
||||
pm.experienceCalc.tsLevelXP[i] = int64(i * i * 80)
|
||||
pm.experienceCalc.vitaeLevels[i] = 1.0 // No vitae by default
|
||||
}
|
||||
}
|
||||
|
||||
// startBackgroundProcessors starts all background processing routines
|
||||
func (pm *PlayerManager) startBackgroundProcessors() {
|
||||
// Start save processor
|
||||
go pm.dbPersister.processSaveQueue()
|
||||
|
||||
// Start event processor
|
||||
go pm.eventManager.processEvents()
|
||||
|
||||
// Start periodic tasks
|
||||
go pm.periodicTasks()
|
||||
}
|
||||
|
||||
// processSaveQueue processes the background save queue
|
||||
func (dp *DatabasePersister) processSaveQueue() {
|
||||
for {
|
||||
select {
|
||||
case player := <-dp.saveQueue:
|
||||
if err := dp.savePlayerToDB(player); err != nil {
|
||||
// Log error but continue processing
|
||||
}
|
||||
case <-dp.stopChan:
|
||||
// Process remaining items in queue
|
||||
for len(dp.saveQueue) > 0 {
|
||||
player := <-dp.saveQueue
|
||||
dp.savePlayerToDB(player)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processEvents processes the background event queue
|
||||
func (em *EventManager) processEvents() {
|
||||
for event := range em.eventQueue {
|
||||
em.eventMutex.RLock()
|
||||
handlers := em.subscribers[event.Type]
|
||||
em.eventMutex.RUnlock()
|
||||
|
||||
for _, handler := range handlers {
|
||||
go handler(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// periodicTasks runs periodic maintenance tasks
|
||||
func (pm *PlayerManager) periodicTasks() {
|
||||
ticker := time.NewTicker(time.Minute * 5)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
pm.performMaintenance()
|
||||
case <-pm.dbPersister.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// performMaintenance performs periodic maintenance tasks
|
||||
func (pm *PlayerManager) performMaintenance() {
|
||||
// Clean up old combat sessions
|
||||
pm.combatSystem.combatMutex.Lock()
|
||||
cutoff := time.Now().Add(-time.Minute * 10)
|
||||
for playerID, session := range pm.combatSystem.activeCombats {
|
||||
if session.StartTime.Before(cutoff) {
|
||||
delete(pm.combatSystem.activeCombats, playerID)
|
||||
}
|
||||
}
|
||||
pm.combatSystem.combatMutex.Unlock()
|
||||
|
||||
pm.logger.Debug("Performed maintenance tasks")
|
||||
}
|
||||
|
||||
// Helper methods for experience calculation
|
||||
func (ec *ExperienceCalculator) getVitaeModifier(level int16) float32 {
|
||||
if level < 0 || int(level) >= len(ec.vitaeLevels) {
|
||||
return 1.0
|
||||
}
|
||||
return ec.vitaeLevels[level]
|
||||
}
|
||||
|
||||
// checkLevelUp checks if a player should level up
|
||||
func (pm *PlayerManager) checkLevelUp(player *Player, xpType string) {
|
||||
currentLevel := player.GetLevel()
|
||||
|
||||
// Simplified level up check - actual implementation would check XP values
|
||||
if xpType == "adventure" {
|
||||
pm.logger.Debug("Would check adventure level up for player %d at level %d", player.charID, currentLevel)
|
||||
} else if xpType == "tradeskill" {
|
||||
pm.logger.Debug("Would check tradeskill level up for player %d", player.charID)
|
||||
}
|
||||
}
|
||||
|
||||
// Packet sending methods
|
||||
func (pm *PlayerManager) sendLevelUpPacket(player *Player) {
|
||||
if pm.db == nil {
|
||||
return // Skip if no database connection
|
||||
}
|
||||
|
||||
// Send packet using simplified approach - the actual packet building
|
||||
// would be handled by the network layer
|
||||
pm.logger.Debug("Sent level up packet for player %d to level %d", player.charID, player.GetLevel())
|
||||
}
|
||||
|
||||
func (pm *PlayerManager) sendXPUpdatePacket(player *Player, xpAmount int64, xpType string) {
|
||||
if pm.db == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pm.logger.Debug("Sent %s XP update packet for player %d: %d", xpType, player.charID, xpAmount)
|
||||
}
|
||||
|
||||
func (pm *PlayerManager) sendCombatPacket(attacker *Player, defenderID int32, damage int32) {
|
||||
if pm.db == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pm.logger.Debug("Sent combat packet for player %d vs %d: %d damage", attacker.charID, defenderID, damage)
|
||||
}
|
||||
|
||||
func (pm *PlayerManager) sendCurrencyUpdatePacket(player *Player, currencyType string, amount int64) {
|
||||
if pm.db == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pm.logger.Debug("Sent currency update packet for player %d: %s +%d", player.charID, currencyType, amount)
|
||||
}
|
||||
|
||||
// Database operations
|
||||
func (dp *DatabasePersister) savePlayerToDB(player *Player) error {
|
||||
if dp.db == nil {
|
||||
return fmt.Errorf("no database connection")
|
||||
}
|
||||
|
||||
dp.saveMutex.Lock()
|
||||
defer dp.saveMutex.Unlock()
|
||||
|
||||
// Get player info
|
||||
info := player.GetInfoStruct()
|
||||
if info == nil {
|
||||
return fmt.Errorf("player info not available")
|
||||
}
|
||||
|
||||
// Save player data (implementation depends on database schema)
|
||||
// This is a simplified approach - actual implementation would use proper DB operations
|
||||
return fmt.Errorf("database save not yet implemented")
|
||||
}
|
||||
|
||||
func (pm *PlayerManager) savePlayerSync(player *Player) error {
|
||||
return pm.dbPersister.savePlayerToDB(player)
|
||||
}
|
||||
|
||||
// Event subscription methods
|
||||
func (pm *PlayerManager) SubscribeToEvent(eventType EventType, handler EventHandler) {
|
||||
pm.eventManager.eventMutex.Lock()
|
||||
defer pm.eventManager.eventMutex.Unlock()
|
||||
|
||||
pm.eventManager.subscribers[eventType] = append(pm.eventManager.subscribers[eventType], handler)
|
||||
}
|
294
internal/player/unified_test.go
Normal file
294
internal/player/unified_test.go
Normal file
@ -0,0 +1,294 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// MockLogger for testing
|
||||
type MockLogger struct{}
|
||||
|
||||
func (ml *MockLogger) Debug(format string, args ...interface{}) {}
|
||||
func (ml *MockLogger) Info(format string, args ...interface{}) {}
|
||||
func (ml *MockLogger) Warn(format string, args ...interface{}) {}
|
||||
func (ml *MockLogger) Error(format string, args ...interface{}) {}
|
||||
|
||||
// MockConfig for testing
|
||||
type MockConfig struct{}
|
||||
|
||||
func (mc *MockConfig) GetString(key string) string { return "" }
|
||||
func (mc *MockConfig) GetInt(key string) int { return 0 }
|
||||
func (mc *MockConfig) GetBool(key string) bool { return false }
|
||||
|
||||
func TestPlayerManagerCreation(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
config := &MockConfig{}
|
||||
var db *database.Database // Use nil for testing
|
||||
|
||||
manager := NewPlayerManager(db, logger, config)
|
||||
if manager == nil {
|
||||
t.Fatal("Failed to create PlayerManager")
|
||||
}
|
||||
|
||||
// Test that manager starts empty
|
||||
players := manager.GetAllPlayers()
|
||||
if len(players) != 0 {
|
||||
t.Errorf("Expected 0 players, got %d", len(players))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerManagerAddRemove(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
config := &MockConfig{}
|
||||
var db *database.Database
|
||||
|
||||
manager := NewPlayerManager(db, logger, config)
|
||||
|
||||
// Create a test player
|
||||
player := NewPlayer()
|
||||
player.charID = 12345
|
||||
player.SetLevel(10)
|
||||
|
||||
// Test adding player
|
||||
manager.AddPlayer(player)
|
||||
|
||||
// Verify player was added
|
||||
retrievedPlayer := manager.GetPlayer(12345)
|
||||
if retrievedPlayer == nil {
|
||||
t.Error("Failed to retrieve added player")
|
||||
}
|
||||
|
||||
if retrievedPlayer.GetCharacterID() != 12345 {
|
||||
t.Errorf("Expected character ID 12345, got %d", retrievedPlayer.GetCharacterID())
|
||||
}
|
||||
|
||||
// Test getting all players
|
||||
allPlayers := manager.GetAllPlayers()
|
||||
if len(allPlayers) != 1 {
|
||||
t.Errorf("Expected 1 player, got %d", len(allPlayers))
|
||||
}
|
||||
|
||||
// Test removing player
|
||||
manager.RemovePlayer(12345)
|
||||
|
||||
removedPlayer := manager.GetPlayer(12345)
|
||||
if removedPlayer != nil {
|
||||
t.Error("Player should have been removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerManagerMultiplePlayers(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
config := &MockConfig{}
|
||||
var db *database.Database
|
||||
|
||||
manager := NewPlayerManager(db, logger, config)
|
||||
|
||||
// Add multiple players
|
||||
for i := 1; i <= 5; i++ {
|
||||
player := NewPlayer()
|
||||
player.charID = int32(i)
|
||||
player.SetLevel(int16(10 * i))
|
||||
manager.AddPlayer(player)
|
||||
}
|
||||
|
||||
// Verify all players were added
|
||||
allPlayers := manager.GetAllPlayers()
|
||||
if len(allPlayers) != 5 {
|
||||
t.Errorf("Expected 5 players, got %d", len(allPlayers))
|
||||
}
|
||||
|
||||
// Test retrieving specific players
|
||||
for i := 1; i <= 5; i++ {
|
||||
player := manager.GetPlayer(int32(i))
|
||||
if player == nil {
|
||||
t.Errorf("Failed to retrieve player %d", i)
|
||||
}
|
||||
if player.GetLevel() != int8(10*i) {
|
||||
t.Errorf("Expected level %d, got %d", 10*i, player.GetLevel())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerManagerExperience(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
config := &MockConfig{}
|
||||
var db *database.Database
|
||||
|
||||
manager := NewPlayerManager(db, logger, config)
|
||||
|
||||
// Create and add a test player
|
||||
player := NewPlayer()
|
||||
player.charID = 999
|
||||
player.SetLevel(5)
|
||||
manager.AddPlayer(player)
|
||||
|
||||
// Test adding experience (should not error even though it's simplified)
|
||||
err := manager.AddExperience(999, 1000, "adventure")
|
||||
if err != nil {
|
||||
t.Errorf("AddExperience failed: %v", err)
|
||||
}
|
||||
|
||||
// Test adding tradeskill experience
|
||||
err = manager.AddExperience(999, 500, "tradeskill")
|
||||
if err != nil {
|
||||
t.Errorf("AddExperience tradeskill failed: %v", err)
|
||||
}
|
||||
|
||||
// Test invalid XP type
|
||||
err = manager.AddExperience(999, 100, "invalid")
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid XP type")
|
||||
}
|
||||
|
||||
// Test non-existent player
|
||||
err = manager.AddExperience(888, 100, "adventure")
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existent player")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerManagerCombat(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
config := &MockConfig{}
|
||||
var db *database.Database
|
||||
|
||||
manager := NewPlayerManager(db, logger, config)
|
||||
|
||||
// Create test players
|
||||
attacker := NewPlayer()
|
||||
attacker.charID = 100
|
||||
manager.AddPlayer(attacker)
|
||||
|
||||
defender := NewPlayer()
|
||||
defender.charID = 200
|
||||
manager.AddPlayer(defender)
|
||||
|
||||
// Test combat processing
|
||||
err := manager.ProcessCombat(100, 200, 50)
|
||||
if err != nil {
|
||||
t.Errorf("ProcessCombat failed: %v", err)
|
||||
}
|
||||
|
||||
// Test combat with non-existent attacker
|
||||
err = manager.ProcessCombat(999, 200, 30)
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existent attacker")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerManagerCurrency(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
config := &MockConfig{}
|
||||
var db *database.Database
|
||||
|
||||
manager := NewPlayerManager(db, logger, config)
|
||||
|
||||
// Create test player
|
||||
player := NewPlayer()
|
||||
player.charID = 300
|
||||
manager.AddPlayer(player)
|
||||
|
||||
// Test currency updates
|
||||
err := manager.UpdateCurrency(300, "coin", 1000)
|
||||
if err != nil {
|
||||
t.Errorf("UpdateCurrency coin failed: %v", err)
|
||||
}
|
||||
|
||||
err = manager.UpdateCurrency(300, "tokens", 50)
|
||||
if err != nil {
|
||||
t.Errorf("UpdateCurrency tokens failed: %v", err)
|
||||
}
|
||||
|
||||
err = manager.UpdateCurrency(300, "status", 25)
|
||||
if err != nil {
|
||||
t.Errorf("UpdateCurrency status failed: %v", err)
|
||||
}
|
||||
|
||||
// Test invalid currency type
|
||||
err = manager.UpdateCurrency(300, "invalid", 100)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid currency type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerManagerLevelUp(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
config := &MockConfig{}
|
||||
var db *database.Database
|
||||
|
||||
manager := NewPlayerManager(db, logger, config)
|
||||
|
||||
// Create test player
|
||||
player := NewPlayer()
|
||||
player.charID = 400
|
||||
player.SetLevel(10)
|
||||
manager.AddPlayer(player)
|
||||
|
||||
// Test level up
|
||||
err := manager.UpdatePlayerLevel(400, 11)
|
||||
if err != nil {
|
||||
t.Errorf("UpdatePlayerLevel failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify level was updated
|
||||
updatedPlayer := manager.GetPlayer(400)
|
||||
if updatedPlayer.GetLevel() != 11 {
|
||||
t.Errorf("Expected level 11, got %d", updatedPlayer.GetLevel())
|
||||
}
|
||||
|
||||
// Test level up for non-existent player
|
||||
err = manager.UpdatePlayerLevel(999, 20)
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existent player")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerManagerSave(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
config := &MockConfig{}
|
||||
var db *database.Database
|
||||
|
||||
manager := NewPlayerManager(db, logger, config)
|
||||
|
||||
// Create test player
|
||||
player := NewPlayer()
|
||||
player.charID = 500
|
||||
manager.AddPlayer(player)
|
||||
|
||||
// Test save player (should not panic even with nil database)
|
||||
err := manager.SavePlayer(500)
|
||||
if err != nil {
|
||||
// Expected since we're using nil database
|
||||
t.Logf("Save failed as expected with nil database: %v", err)
|
||||
}
|
||||
|
||||
// Test save all players
|
||||
err = manager.SaveAllPlayers()
|
||||
if err != nil {
|
||||
t.Errorf("SaveAllPlayers failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerManagerShutdown(t *testing.T) {
|
||||
logger := &MockLogger{}
|
||||
config := &MockConfig{}
|
||||
var db *database.Database
|
||||
|
||||
manager := NewPlayerManager(db, logger, config)
|
||||
|
||||
// Add some test players
|
||||
for i := 1; i <= 3; i++ {
|
||||
player := NewPlayer()
|
||||
player.charID = int32(600 + i)
|
||||
manager.AddPlayer(player)
|
||||
}
|
||||
|
||||
// Test graceful shutdown (should not panic)
|
||||
manager.Shutdown()
|
||||
|
||||
// Give a moment for background processes to stop
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user