first commit
This commit is contained in:
249
internal/player/player.go
Normal file
249
internal/player/player.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"a301_game_server/internal/combat"
|
||||
"a301_game_server/internal/db/repository"
|
||||
"a301_game_server/internal/entity"
|
||||
"a301_game_server/internal/network"
|
||||
"a301_game_server/pkg/mathutil"
|
||||
pb "a301_game_server/proto/gen/pb"
|
||||
)
|
||||
|
||||
// Player represents an online player in the game world.
|
||||
type Player struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
id uint64
|
||||
charID int64 // database character ID
|
||||
acctID int64 // database account ID
|
||||
name string
|
||||
position mathutil.Vec3
|
||||
rotation float32
|
||||
velocity mathutil.Vec3
|
||||
|
||||
stats Stats
|
||||
|
||||
conn *network.Connection
|
||||
zoneID uint32
|
||||
session *Session
|
||||
dirty bool // true if state changed since last DB save
|
||||
}
|
||||
|
||||
// NewPlayer creates a new player.
|
||||
func NewPlayer(id uint64, name string, conn *network.Connection) *Player {
|
||||
return &Player{
|
||||
id: id,
|
||||
name: name,
|
||||
conn: conn,
|
||||
stats: Stats{
|
||||
HP: 100, MaxHP: 100,
|
||||
MP: 50, MaxMP: 50,
|
||||
Str: 10, Dex: 10, Int: 10,
|
||||
Level: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewPlayerFromDB creates a player from persisted character data.
|
||||
func NewPlayerFromDB(data *repository.CharacterData, conn *network.Connection) *Player {
|
||||
return &Player{
|
||||
id: uint64(data.ID),
|
||||
charID: data.ID,
|
||||
acctID: data.AccountID,
|
||||
name: data.Name,
|
||||
position: mathutil.NewVec3(data.PosX, data.PosY, data.PosZ),
|
||||
rotation: data.Rotation,
|
||||
stats: Stats{
|
||||
HP: data.HP, MaxHP: data.MaxHP,
|
||||
MP: data.MP, MaxMP: data.MaxMP,
|
||||
Str: data.Str, Dex: data.Dex, Int: data.IntStat,
|
||||
Level: data.Level, Exp: data.Exp,
|
||||
},
|
||||
conn: conn,
|
||||
zoneID: uint32(data.ZoneID),
|
||||
}
|
||||
}
|
||||
|
||||
// ToCharacterData converts current state to a persistable format.
|
||||
func (p *Player) ToCharacterData() *repository.CharacterData {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return &repository.CharacterData{
|
||||
ID: p.charID,
|
||||
AccountID: p.acctID,
|
||||
Name: p.name,
|
||||
Level: p.stats.Level,
|
||||
Exp: p.stats.Exp,
|
||||
HP: p.stats.HP,
|
||||
MaxHP: p.stats.MaxHP,
|
||||
MP: p.stats.MP,
|
||||
MaxMP: p.stats.MaxMP,
|
||||
Str: p.stats.Str,
|
||||
Dex: p.stats.Dex,
|
||||
IntStat: p.stats.Int,
|
||||
ZoneID: int32(p.zoneID),
|
||||
PosX: p.position.X,
|
||||
PosY: p.position.Y,
|
||||
PosZ: p.position.Z,
|
||||
Rotation: p.rotation,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) EntityID() uint64 { return p.id }
|
||||
func (p *Player) EntityType() entity.Type { return entity.TypePlayer }
|
||||
|
||||
func (p *Player) Position() mathutil.Vec3 {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.position
|
||||
}
|
||||
|
||||
func (p *Player) SetPosition(pos mathutil.Vec3) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.position = pos
|
||||
p.dirty = true
|
||||
}
|
||||
|
||||
func (p *Player) Rotation() float32 {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.rotation
|
||||
}
|
||||
|
||||
func (p *Player) SetRotation(rot float32) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.rotation = rot
|
||||
p.dirty = true
|
||||
}
|
||||
|
||||
func (p *Player) Velocity() mathutil.Vec3 {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.velocity
|
||||
}
|
||||
|
||||
func (p *Player) SetVelocity(vel mathutil.Vec3) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.velocity = vel
|
||||
}
|
||||
|
||||
func (p *Player) Name() string { return p.name }
|
||||
func (p *Player) CharID() int64 { return p.charID }
|
||||
func (p *Player) AccountID() int64 { return p.acctID }
|
||||
func (p *Player) Connection() *network.Connection { return p.conn }
|
||||
func (p *Player) ZoneID() uint32 { return p.zoneID }
|
||||
func (p *Player) SetZoneID(id uint32) { p.zoneID = id }
|
||||
|
||||
func (p *Player) Stats() combat.CombatStats {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return combat.CombatStats{
|
||||
Str: p.stats.Str,
|
||||
Dex: p.stats.Dex,
|
||||
Int: p.stats.Int,
|
||||
Level: p.stats.Level,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) RawStats() Stats {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.stats
|
||||
}
|
||||
|
||||
func (p *Player) SetStats(s Stats) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.stats = s
|
||||
p.dirty = true
|
||||
}
|
||||
|
||||
func (p *Player) HP() int32 {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.stats.HP
|
||||
}
|
||||
|
||||
func (p *Player) SetHP(hp int32) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if hp < 0 {
|
||||
hp = 0
|
||||
}
|
||||
if hp > p.stats.MaxHP {
|
||||
hp = p.stats.MaxHP
|
||||
}
|
||||
p.stats.HP = hp
|
||||
p.dirty = true
|
||||
}
|
||||
|
||||
func (p *Player) MaxHP() int32 {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.stats.MaxHP
|
||||
}
|
||||
|
||||
func (p *Player) MP() int32 {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.stats.MP
|
||||
}
|
||||
|
||||
func (p *Player) SetMP(mp int32) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if mp < 0 {
|
||||
mp = 0
|
||||
}
|
||||
if mp > p.stats.MaxMP {
|
||||
mp = p.stats.MaxMP
|
||||
}
|
||||
p.stats.MP = mp
|
||||
p.dirty = true
|
||||
}
|
||||
|
||||
func (p *Player) Level() int32 {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.stats.Level
|
||||
}
|
||||
|
||||
func (p *Player) IsAlive() bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.stats.HP > 0
|
||||
}
|
||||
|
||||
// IsDirty returns true if state has changed since last save.
|
||||
func (p *Player) IsDirty() bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.dirty
|
||||
}
|
||||
|
||||
// ClearDirty resets the dirty flag after a successful save.
|
||||
func (p *Player) ClearDirty() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.dirty = false
|
||||
}
|
||||
|
||||
func (p *Player) ToProto() *pb.EntityState {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return &pb.EntityState{
|
||||
EntityId: p.id,
|
||||
Name: p.name,
|
||||
Position: &pb.Vector3{X: p.position.X, Y: p.position.Y, Z: p.position.Z},
|
||||
Rotation: p.rotation,
|
||||
Hp: p.stats.HP,
|
||||
MaxHp: p.stats.MaxHP,
|
||||
Level: p.stats.Level,
|
||||
EntityType: pb.EntityType_ENTITY_TYPE_PLAYER,
|
||||
}
|
||||
}
|
||||
90
internal/player/session.go
Normal file
90
internal/player/session.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Session holds an authenticated player's session state.
|
||||
type Session struct {
|
||||
Token string
|
||||
PlayerID uint64
|
||||
PlayerName string
|
||||
CreatedAt time.Time
|
||||
LastActive time.Time
|
||||
}
|
||||
|
||||
// SessionManager manages active sessions.
|
||||
type SessionManager struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*Session // token -> session
|
||||
}
|
||||
|
||||
// NewSessionManager creates a new session manager.
|
||||
func NewSessionManager() *SessionManager {
|
||||
return &SessionManager{
|
||||
sessions: make(map[string]*Session),
|
||||
}
|
||||
}
|
||||
|
||||
// Create generates a new session for the given player.
|
||||
func (sm *SessionManager) Create(playerID uint64, playerName string) *Session {
|
||||
token := generateToken()
|
||||
now := time.Now()
|
||||
|
||||
s := &Session{
|
||||
Token: token,
|
||||
PlayerID: playerID,
|
||||
PlayerName: playerName,
|
||||
CreatedAt: now,
|
||||
LastActive: now,
|
||||
}
|
||||
|
||||
sm.mu.Lock()
|
||||
sm.sessions[token] = s
|
||||
sm.mu.Unlock()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Get retrieves a session by token. Returns nil if not found.
|
||||
func (sm *SessionManager) Get(token string) *Session {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
s := sm.sessions[token]
|
||||
if s != nil {
|
||||
s.LastActive = time.Now()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Remove deletes a session.
|
||||
func (sm *SessionManager) Remove(token string) {
|
||||
sm.mu.Lock()
|
||||
delete(sm.sessions, token)
|
||||
sm.mu.Unlock()
|
||||
}
|
||||
|
||||
// CleanupExpired removes sessions inactive for longer than the given duration.
|
||||
func (sm *SessionManager) CleanupExpired(maxIdle time.Duration) int {
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
|
||||
cutoff := time.Now().Add(-maxIdle)
|
||||
removed := 0
|
||||
for token, s := range sm.sessions {
|
||||
if s.LastActive.Before(cutoff) {
|
||||
delete(sm.sessions, token)
|
||||
removed++
|
||||
}
|
||||
}
|
||||
return removed
|
||||
}
|
||||
|
||||
func generateToken() string {
|
||||
b := make([]byte, 32)
|
||||
_, _ = rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
14
internal/player/stats.go
Normal file
14
internal/player/stats.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package player
|
||||
|
||||
// Stats holds combat-related attributes.
|
||||
type Stats struct {
|
||||
HP int32
|
||||
MaxHP int32
|
||||
MP int32
|
||||
MaxMP int32
|
||||
Str int32
|
||||
Dex int32
|
||||
Int int32
|
||||
Level int32
|
||||
Exp int64
|
||||
}
|
||||
Reference in New Issue
Block a user