diff --git a/go.mod b/go.mod index 1fda214..d3a56ba 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module mc-player-service go 1.21 require ( - github.com/emortalmc/proto-specs/gen/go v0.0.0-20231212225453-a8938507297c + github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406012921-6a9ad1aff227 github.com/google/uuid v1.5.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/segmentio/kafka-go v0.4.46 diff --git a/go.sum b/go.sum index 64ae83d..c7bc2d8 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,22 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emortalmc/proto-specs/gen/go v0.0.0-20231212225453-a8938507297c h1:aXdLcj1nR3VAApCuHnOsSwzdIAZcMTm2LofIebfld5E= github.com/emortalmc/proto-specs/gen/go v0.0.0-20231212225453-a8938507297c/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240317132218-b310d52fd037 h1:t9Ct3hfgdKycSyMxytNFwFCpBYkEF5vHh1RLisBS0Yw= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240317132218-b310d52fd037/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240404195153-6dc4f4e385fc h1:qpIu8E1P/CkOZJ/XOcQnGWEISONjyn2fD4lj/F/ercY= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240404195153-6dc4f4e385fc/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240405194818-f8231a77d2e5 h1:npqo8fIHM3jpHf73jtkTz9bz1EWn3NC+pAM0jMHiT0s= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240405194818-f8231a77d2e5/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406001747-c86b99d5483d h1:LJxka1f1NRbIi4pKa9yTaCUqMNtgb5wEBdsBiWX7CNM= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406001747-c86b99d5483d/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406011652-11c567b404af h1:bctA2fXlQK9awuH6Oksav5fUwA9ESBfLrXNohM+gPrs= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406011652-11c567b404af/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406011759-782e66e8982a h1:9KbJ38ys1yri97anPeSJMiWsqJ+qWMUr2sGFCzdvlT8= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406011759-782e66e8982a/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406012812-1b77be1d5f2c h1:AUHtqD+LE49fFmtro7HpkcSy3gPv0StGrKmpTOXOCjQ= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406012812-1b77be1d5f2c/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406012921-6a9ad1aff227 h1:KXL6uPezjaPVPmlYE9UY4MzkSdqvo4L7gVTd/wQA96g= +github.com/emortalmc/proto-specs/gen/go v0.0.0-20240406012921-6a9ad1aff227/go.mod h1:se+tHcK9FWxeadkxLF5uj+SPauEye0X+Iq6cGczXGJY= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/internal/app/mc_player.go b/internal/app/mc_player.go index fe3ac05..c30ced8 100644 --- a/internal/app/mc_player.go +++ b/internal/app/mc_player.go @@ -7,7 +7,8 @@ import ( "mc-player-service/internal/app/player" "mc-player-service/internal/config" "mc-player-service/internal/grpc" - "mc-player-service/internal/kafka" + "mc-player-service/internal/kafka/consumer" + kafkaWriter "mc-player-service/internal/kafka/writer" "mc-player-service/internal/repository" "os/signal" "sync" @@ -33,12 +34,14 @@ func Run(cfg config.Config, log *zap.SugaredLogger) { log.Fatalw("failed to create repository", err) } + notifier := kafkaWriter.NewKafkaNotifier(ctx, wg, cfg.Kafka, log) + badgeSvc := badge.NewService(log, repo, repo, badgeCfg) - playerSvc := player.NewService(log, repo, cfg) + playerSvc := player.NewService(log, cfg, repo, notifier) - kafka.NewConsumer(ctx, wg, cfg, log, repo, badgeSvc, playerSvc) + kafkaConsumer.NewConsumer(ctx, wg, cfg, log, repo, badgeSvc, playerSvc) - grpc.RunServices(ctx, log, wg, cfg, badgeSvc, badgeCfg, repo) + grpc.RunServices(ctx, log, wg, cfg, badgeSvc, badgeCfg, playerSvc, repo) wg.Wait() log.Info("shutting down") diff --git a/internal/app/player/kafka_writer.go b/internal/app/player/kafka_writer.go new file mode 100644 index 0000000..86a69cf --- /dev/null +++ b/internal/app/player/kafka_writer.go @@ -0,0 +1,15 @@ +package player + +import ( + "context" + "github.com/google/uuid" + kafkaWriter "mc-player-service/internal/kafka/writer" +) + +var ( + _ KafkaWriter = &kafkaWriter.Notifier{} +) + +type KafkaWriter interface { + PlayerExperienceChange(ctx context.Context, playerID uuid.UUID, reason string, oldXP int, newXP int, oldLevel int, newLevel int) +} diff --git a/internal/app/player/service.go b/internal/app/player/service.go index 78eea51..4cd245b 100644 --- a/internal/app/player/service.go +++ b/internal/app/player/service.go @@ -2,33 +2,64 @@ package player import ( "context" + "fmt" "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson/primitive" "go.uber.org/zap" "mc-player-service/internal/config" "mc-player-service/internal/repository" "mc-player-service/internal/repository/model" + "mc-player-service/internal/utils/experience" "mc-player-service/internal/webhook" "time" ) type Service interface { HandlePlayerConnect(ctx context.Context, time time.Time, playerID uuid.UUID, playerUsername string, - proxyID string, playerSkin model.PlayerSkin, player model.Player) + proxyID string, playerSkin model.PlayerSkin, player model.Player) HandlePlayerDisconnect(ctx context.Context, time time.Time, playerID uuid.UUID, playerUsername string) HandlePlayerServerSwitch(ctx context.Context, pID uuid.UUID, newServerID string) + + AddExperienceByID(ctx context.Context, playerID uuid.UUID, reason string, amount int) (int, error) } type serviceImpl struct { log *zap.SugaredLogger - repo repository.PlayerReadWriter + repo repository.PlayerReadWriter + kafkaW KafkaWriter webhook webhook.Webhook } -func NewService(log *zap.SugaredLogger, repo repository.PlayerReadWriter, cfg config.Config) Service { +func NewService(log *zap.SugaredLogger, cfg config.Config, repo repository.PlayerReadWriter, kafkaW KafkaWriter) Service { return &serviceImpl{ - log: log, - repo: repo, + log: log, + repo: repo, + kafkaW: kafkaW, webhook: webhook.NewWebhook(cfg.DiscordWebhookUrl, log), } } + +func (s *serviceImpl) AddExperienceByID(ctx context.Context, playerID uuid.UUID, reason string, amount int) (int, error) { + newXP, err := s.repo.AddExperienceToPlayer(ctx, playerID, amount) + if err != nil { + return 0, fmt.Errorf("failed to add experience to player: %w", err) + } + + if err := s.repo.CreateExperienceTransaction(ctx, model.ExperienceTransaction{ + ID: primitive.NewObjectID(), + PlayerID: playerID, + Amount: int64(amount), + Reason: reason, + }); err != nil { + return 0, fmt.Errorf("failed to create experience transaction: %w", err) + } + + oldXP := newXP - amount + oldLevel := experience.XPToLevel(oldXP) + newLevel := experience.XPToLevel(newXP) + + s.kafkaW.PlayerExperienceChange(ctx, playerID, reason, oldXP, newXP, oldLevel, newLevel) + + return newXP, nil +} diff --git a/internal/config/global.go b/internal/config/global.go index 278c705..809fcb9 100644 --- a/internal/config/global.go +++ b/internal/config/global.go @@ -6,8 +6,8 @@ import ( ) type Config struct { - Kafka *KafkaConfig - MongoDB *MongoDBConfig + Kafka KafkaConfig + MongoDB MongoDBConfig Development bool diff --git a/internal/grpc/mc_player.go b/internal/grpc/mc_player.go index 4df4780..21a5be6 100644 --- a/internal/grpc/mc_player.go +++ b/internal/grpc/mc_player.go @@ -11,6 +11,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "mc-player-service/internal/app/player" "mc-player-service/internal/repository" "mc-player-service/internal/repository/model" "mc-player-service/internal/utils" @@ -20,11 +21,13 @@ type mcPlayerService struct { pb.McPlayerServer repo repository.PlayerReader + svc player.Service } -func newMcPlayerService(repo repository.PlayerReader) pb.McPlayerServer { +func newMcPlayerService(repo repository.PlayerReader, svc player.Service) pb.McPlayerServer { return &mcPlayerService{ repo: repo, + svc: svc, } } @@ -177,6 +180,38 @@ func (s *mcPlayerService) GetStatTotalPlaytime(ctx context.Context, _ *pb.GetSta return &pb.GetStatTotalPlaytimeResponse{PlaytimeHours: count}, nil } +func (s *mcPlayerService) AddExperienceToPlayers(ctx context.Context, req *pb.AddExperienceToPlayersRequest) (*pb.AddExperienceToPlayersResponse, error) { + ids := make([]uuid.UUID, len(req.PlayerIds)) + for i, id := range req.PlayerIds { + pId, err := uuid.Parse(id) + if err != nil { + return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid player id %s", id)) + } + + ids[i] = pId + } + + newXPs := make(map[string]uint64, len(ids)) + + for _, id := range ids { + newXP, err := s.svc.AddExperienceByID(ctx, id, req.Reason, int(req.Experience)); + + if err != nil { + return nil, fmt.Errorf("error adding experience to player %s: %w", id.String(), err) + } + + newXPs[id.String()] = uint64(newXP) + } + + return &pb.AddExperienceToPlayersResponse{ + Experience: newXPs, + }, nil +} + +func (s *mcPlayerService) GetPlayerExperience(ctx context.Context, req *pb.GetPlayerExperienceRequest) (*pb.GetPlayerExperienceResponse, error) { + panic("implement me") +} + func (s *mcPlayerService) getOrCreateMcPlayer(ctx context.Context, pId uuid.UUID) (*mcplayer.McPlayer, error) { p, err := s.repo.GetPlayer(ctx, pId) if err != nil { diff --git a/internal/grpc/public.go b/internal/grpc/public.go index b2dbdd0..23fe89a 100644 --- a/internal/grpc/public.go +++ b/internal/grpc/public.go @@ -13,6 +13,7 @@ import ( "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" "mc-player-service/internal/app/badge" + "mc-player-service/internal/app/player" "mc-player-service/internal/config" "mc-player-service/internal/healthprovider" "mc-player-service/internal/repository" @@ -21,7 +22,7 @@ import ( ) func RunServices(ctx context.Context, log *zap.SugaredLogger, wg *sync.WaitGroup, cfg config.Config, - badgeSvc badge.Service, badgeCfg config.BadgeConfig, repo repository.Repository) { + badgeSvc badge.Service, badgeCfg config.BadgeConfig, playerSvc player.Service, repo repository.Repository) { lis, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port)) if err != nil { @@ -45,7 +46,7 @@ func RunServices(ctx context.Context, log *zap.SugaredLogger, wg *sync.WaitGroup healthSrv := healthprovider.Create(ctx, repo) grpc_health_v1.RegisterHealthServer(s, healthSrv) - mcplayer.RegisterMcPlayerServer(s, newMcPlayerService(repo)) + mcplayer.RegisterMcPlayerServer(s, newMcPlayerService(repo, playerSvc)) badgeProto.RegisterBadgeManagerServer(s, newBadgeService(repo, badgeSvc, badgeCfg)) mcplayer.RegisterPlayerTrackerServer(s, newPlayerTrackerService(repo)) log.Infow("listening for gRPC requests", "port", cfg.Port) diff --git a/internal/kafka/consumer.go b/internal/kafka/consumer/consumer.go similarity index 99% rename from internal/kafka/consumer.go rename to internal/kafka/consumer/consumer.go index b9a2541..b5e6a26 100644 --- a/internal/kafka/consumer.go +++ b/internal/kafka/consumer/consumer.go @@ -1,4 +1,4 @@ -package kafka +package kafkaConsumer import ( "context" diff --git a/internal/kafka/writer/writer.go b/internal/kafka/writer/writer.go new file mode 100644 index 0000000..50e574e --- /dev/null +++ b/internal/kafka/writer/writer.go @@ -0,0 +1,75 @@ +package kafkaWriter + +import ( + "context" + "fmt" + "github.com/emortalmc/proto-specs/gen/go/model/mcplayer" + "github.com/google/uuid" + "github.com/segmentio/kafka-go" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + "mc-player-service/internal/config" + "sync" + "time" +) + +const experienceWriterTopic = "player-experience" + +type Notifier struct { + logger *zap.SugaredLogger + w *kafka.Writer +} + +func NewKafkaNotifier(ctx context.Context, wg *sync.WaitGroup, cfg config.KafkaConfig, logger *zap.SugaredLogger) *Notifier { + w := &kafka.Writer{ + Addr: kafka.TCP(cfg.Host), + Topic: experienceWriterTopic, + Balancer: &kafka.LeastBytes{}, + Async: true, + BatchTimeout: 500 * time.Millisecond, + ErrorLogger: kafka.LoggerFunc(logger.Errorw), + } + + wg.Add(1) + go func() { + defer wg.Done() + <-ctx.Done() + if err := w.Close(); err != nil { + logger.Errorw("failed to close kafka writer", "err", err) + } + }() + + return &Notifier{ + logger: logger, + w: w, + } +} + +func (n *Notifier) PlayerExperienceChange(ctx context.Context, playerID uuid.UUID, reason string, oldXP int, newXP int, oldLevel int, newLevel int) { + msg := &mcplayer.PlayerExperienceChangeMessage{ + PlayerId: playerID.String(), + Reason: reason, + PreviousExperience: int64(oldXP), + NewExperience: int64(newXP), + PreviousLevel: int32(oldLevel), + NewLevel: int32(newLevel), + } + + if err := n.writeMessage(ctx, msg); err != nil { + n.logger.Errorw("failed to write message", "err", err) + return + } +} + +func (n *Notifier) writeMessage(ctx context.Context, msg proto.Message) error { + bytes, err := proto.Marshal(msg) + if err != nil { + return fmt.Errorf("failed to marshal proto to bytes: %s", err) + } + + return n.w.WriteMessages(ctx, kafka.Message{ + Topic: experienceWriterTopic, + Headers: []kafka.Header{{Key: "X-Proto-Type", Value: []byte(msg.ProtoReflect().Descriptor().FullName())}}, + Value: bytes, + }) +} diff --git a/internal/repository/model/model.go b/internal/repository/model/model.go index 3ae3345..fda88d7 100644 --- a/internal/repository/model/model.go +++ b/internal/repository/model/model.go @@ -28,6 +28,8 @@ type Player struct { ActiveBadge *string `bson:"activeBadge,omitempty"` CurrentServer *CurrentServer `bson:"currentServer,omitempty"` + + Experience int64 `bson:"experience,omitempty"` } func (p Player) IsEmpty() bool { @@ -45,6 +47,7 @@ func (p Player) ToProto(session LoginSession) *mcplayer.McPlayer { HistoricPlayTime: durationpb.New(p.TotalPlaytime), CurrentServer: p.CurrentServer.ToProto(), CurrentSkin: p.CurrentSkin.ToProto(), + Experience: uint64(p.Experience), } } @@ -177,3 +180,10 @@ type PlayerUsername struct { PlayerID uuid.UUID `bson:"playerId"` Username string `bson:"username"` } + +type ExperienceTransaction struct { + ID primitive.ObjectID `bson:"_id"` + PlayerID uuid.UUID `bson:"playerId"` + Amount int64 `bson:"amount"` + Reason string `bson:"reason"` +} diff --git a/internal/repository/mongo.go b/internal/repository/mongo.go index 75ddd05..1842c9e 100644 --- a/internal/repository/mongo.go +++ b/internal/repository/mongo.go @@ -17,20 +17,22 @@ import ( const ( databaseName = "mc-player-service" - playerCollectionName = "player" - sessionCollectionName = "loginSession" - usernameCollectionName = "playerUsername" + playerCollectionName = "player" + sessionCollectionName = "loginSession" + usernameCollectionName = "playerUsername" + experienceTransactionCollectionName = "experienceTransaction" ) type mongoRepository struct { database *mongo.Database - playerCollection *mongo.Collection - sessionCollection *mongo.Collection - usernameCollection *mongo.Collection + playerCollection *mongo.Collection + sessionCollection *mongo.Collection + usernameCollection *mongo.Collection + experienceTransactionCollection *mongo.Collection } -func NewMongoRepository(ctx context.Context, log *zap.SugaredLogger, wg *sync.WaitGroup, cfg *config.MongoDBConfig) (Repository, error) { +func NewMongoRepository(ctx context.Context, log *zap.SugaredLogger, wg *sync.WaitGroup, cfg config.MongoDBConfig) (Repository, error) { client, err := mongo.Connect(ctx, options.Client().ApplyURI(cfg.URI).SetRegistry(createCodecRegistry())) if err != nil { return nil, err @@ -38,10 +40,11 @@ func NewMongoRepository(ctx context.Context, log *zap.SugaredLogger, wg *sync.Wa database := client.Database(databaseName) repo := &mongoRepository{ - database: database, - playerCollection: database.Collection(playerCollectionName), - sessionCollection: database.Collection(sessionCollectionName), - usernameCollection: database.Collection(usernameCollectionName), + database: database, + playerCollection: database.Collection(playerCollectionName), + sessionCollection: database.Collection(sessionCollectionName), + usernameCollection: database.Collection(usernameCollectionName), + experienceTransactionCollection: database.Collection(experienceTransactionCollectionName), } wg.Add(1) @@ -108,13 +111,21 @@ var ( Options: options.Index().SetName("playerId"), }, } + + experienceTransactionIndexes = []mongo.IndexModel{ + { + Keys: bson.M{"playerId": 1}, + Options: options.Index().SetName("playerId"), + }, + } ) func (m *mongoRepository) createIndexes(ctx context.Context) { collIndexes := map[*mongo.Collection][]mongo.IndexModel{ - m.playerCollection: playerIndexes, - m.sessionCollection: sessionIndexes, - m.usernameCollection: usernameIndexes, + m.playerCollection: playerIndexes, + m.sessionCollection: sessionIndexes, + m.usernameCollection: usernameIndexes, + m.experienceTransactionCollection: experienceTransactionIndexes, } wg := sync.WaitGroup{} diff --git a/internal/repository/mongo_mc_player.go b/internal/repository/mongo_mc_player.go index a120459..2000f1c 100644 --- a/internal/repository/mongo_mc_player.go +++ b/internal/repository/mongo_mc_player.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" "github.com/emortalmc/proto-specs/gen/go/model/common" "github.com/google/uuid" "go.mongodb.org/mongo-driver/bson" @@ -218,6 +219,33 @@ func (m *mongoRepository) CreatePlayerUsername(ctx context.Context, username mod _, err := m.usernameCollection.InsertOne(ctx, username) return err } +func (m *mongoRepository) AddExperienceToPlayer(ctx context.Context, playerID uuid.UUID, experience int) (int, error) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + result := m.playerCollection.FindOneAndUpdate(ctx, bson.M{"_id": playerID}, bson.M{"$inc": bson.M{"experience": experience}}, + options.FindOneAndUpdate().SetReturnDocument(options.After).SetProjection(bson.M{"experience": 1})) + if result.Err() != nil { + return 0, fmt.Errorf("error adding experience to player: %w", result.Err()) + } + + var experienceResult struct { + Experience int `bson:"experience"` + } + if err := result.Decode(&experienceResult); err != nil { + return 0, fmt.Errorf("error decoding player: %w", err) + } + + return experienceResult.Experience, nil +} + +func (m *mongoRepository) CreateExperienceTransaction(ctx context.Context, transaction model.ExperienceTransaction) error { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + _, err := m.experienceTransactionCollection.InsertOne(ctx, transaction) + return err +} func (m *mongoRepository) GetTotalUniquePlayers(ctx context.Context) (int64, error) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) diff --git a/internal/repository/public.go b/internal/repository/public.go index 5df7464..451e7c2 100644 --- a/internal/repository/public.go +++ b/internal/repository/public.go @@ -48,10 +48,10 @@ type PlayerReader interface { // 1. the given server if present // 2. the given fleets if present // 3. globally if neither are present - GetPlayerCount(ctx context.Context, serverId *string, fleetNames []string) (int64, error) + GetPlayerCount(ctx context.Context, serverID *string, fleetNames []string) (int64, error) // GetOnlinePlayers functions the same as GetPlayerCount - GetOnlinePlayers(ctx context.Context, serverId *string, fleetNames []string) ([]model.OnlinePlayer, error) + GetOnlinePlayers(ctx context.Context, serverID *string, fleetNames []string) ([]model.OnlinePlayer, error) GetFleetPlayerCounts(ctx context.Context, fleetNames []string) (map[string]int64, error) @@ -61,12 +61,15 @@ type PlayerReader interface { type PlayerWriter interface { SavePlayer(ctx context.Context, player model.Player, upsert bool) error - PlayerLogout(ctx context.Context, playerId uuid.UUID, lastOnline time.Time, addedPlaytime time.Duration) error + PlayerLogout(ctx context.Context, playerID uuid.UUID, lastOnline time.Time, addedPlaytime time.Duration) error CreateLoginSession(ctx context.Context, session model.LoginSession) error - SetLoginSessionLogoutTime(ctx context.Context, playerId uuid.UUID, logoutTime time.Time) error + SetLoginSessionLogoutTime(ctx context.Context, playerID uuid.UUID, logoutTime time.Time) error CreatePlayerUsername(ctx context.Context, username model.PlayerUsername) error + AddExperienceToPlayer(ctx context.Context, playerID uuid.UUID, experience int) (int, error) + CreateExperienceTransaction(ctx context.Context, transaction model.ExperienceTransaction) error + SetPlayerServerAndFleet(ctx context.Context, playerId uuid.UUID, serverId string, fleet string) error } diff --git a/internal/utils/experience/experience.go b/internal/utils/experience/experience.go new file mode 100644 index 0000000..c6cd660 --- /dev/null +++ b/internal/utils/experience/experience.go @@ -0,0 +1,18 @@ +package experience + +import "math" + +const ( + a = 0.15 + b = float64(2) +) + +func XPToLevel(xp int) int { + level := a * math.Pow(float64(xp), 1/b) + return int(level) +} + +func LevelToXP(level int) int { + xp := math.Pow(float64(level)/a, b) + return int(math.Ceil(xp)) +}