feat: leveling system
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
"velox-bot/internal/config"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
func NewDB(config *config.Config) (*sql.DB, error) {
|
||||
pool, err := sql.Open("pgx", config.DBHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pool.SetMaxIdleConns(10)
|
||||
pool.SetMaxOpenConns(100)
|
||||
pool.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
if err := pool.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package levelrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Level struct {
|
||||
GuildID int64
|
||||
UserID int64
|
||||
Level int
|
||||
XP int64
|
||||
}
|
||||
|
||||
type Repo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewRepo(db *sql.DB) *Repo {
|
||||
return &Repo{db: db}
|
||||
}
|
||||
|
||||
func (r *Repo) GetLevel(ctx context.Context, guildID, userID int64) (*Level, error) {
|
||||
const q = `
|
||||
SELECT level, xp
|
||||
FROM levels
|
||||
WHERE guild = $1 AND user_id = $2
|
||||
`
|
||||
row := r.db.QueryRowContext(ctx, q, guildID, userID)
|
||||
var lvl Level
|
||||
lvl.GuildID = guildID
|
||||
lvl.UserID = userID
|
||||
switch err := row.Scan(&lvl.Level, &lvl.XP); err {
|
||||
case sql.ErrNoRows:
|
||||
// default new user
|
||||
lvl.Level = 0
|
||||
lvl.XP = 0
|
||||
return &lvl, nil
|
||||
case nil:
|
||||
return &lvl, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
func (r *Repo) UpsertLevel(ctx context.Context, lvl *Level) error {
|
||||
const q = `
|
||||
INSERT INTO levels (guild, user_id, level, xp)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (guild, user_id)
|
||||
DO UPDATE SET level = EXCLUDED.level, xp = EXCLUDED.xp
|
||||
`
|
||||
_, err := r.db.ExecContext(ctx, q, lvl.GuildID, lvl.UserID, lvl.Level, lvl.XP)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repo) SetLevel(ctx context.Context, guildID, userID int64, level int) error {
|
||||
const q = `
|
||||
INSERT INTO levels (guild, user_id, level, xp)
|
||||
VALUES ($1, $2, $3, 99)
|
||||
ON CONFLICT (guild, user_id)
|
||||
DO UPDATE SET level = EXCLUDED.level, xp = 99
|
||||
`
|
||||
_, err := r.db.ExecContext(ctx, q, guildID, userID, level)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repo) TopLevels(ctx context.Context, guildID int64, limit int) ([]*Level, error) {
|
||||
const q = `
|
||||
SELECT user_id, level, xp
|
||||
FROM levels
|
||||
WHERE guild = $1
|
||||
ORDER BY level DESC, xp DESC, user_id ASC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, q, guildID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]*Level, 0, limit)
|
||||
for rows.Next() {
|
||||
lvl := &Level{GuildID: guildID}
|
||||
if err := rows.Scan(&lvl.UserID, &lvl.Level, &lvl.XP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, lvl)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package settingsrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Repo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewRepo(db *sql.DB) *Repo {
|
||||
return &Repo{db: db}
|
||||
}
|
||||
|
||||
func (r *Repo) SetLevelSystemEnabled(ctx context.Context, guildID int64, enabled bool) error {
|
||||
const q = `
|
||||
INSERT INTO levelsettings (guild_id, levelsys)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (guild_id)
|
||||
DO UPDATE SET levelsys = EXCLUDED.levelsys
|
||||
`
|
||||
_, err := r.db.ExecContext(ctx, q, guildID, enabled)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repo) IsLevelSystemEnabled(ctx context.Context, guildID int64) (bool, error) {
|
||||
const q = `
|
||||
SELECT levelsys
|
||||
FROM levelsettings
|
||||
WHERE guild_id = $1
|
||||
`
|
||||
row := r.db.QueryRowContext(ctx, q, guildID)
|
||||
var enabled bool
|
||||
switch err := row.Scan(&enabled); err {
|
||||
case nil:
|
||||
return enabled, nil
|
||||
case sql.ErrNoRows:
|
||||
return false, nil
|
||||
default:
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repo) GetLevelupChannelID(ctx context.Context, guildID int64) (int64, bool, error) {
|
||||
const q = `
|
||||
SELECT levelup_channel_id
|
||||
FROM levelup
|
||||
WHERE guild_id = $1
|
||||
`
|
||||
row := r.db.QueryRowContext(ctx, q, guildID)
|
||||
var channelID sql.NullInt64
|
||||
switch err := row.Scan(&channelID); err {
|
||||
case nil:
|
||||
if !channelID.Valid {
|
||||
return 0, false, nil
|
||||
}
|
||||
return channelID.Int64, true, nil
|
||||
case sql.ErrNoRows:
|
||||
return 0, false, nil
|
||||
default:
|
||||
return 0, false, err
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repo) GetRoleRewardForLevel(ctx context.Context, guildID int64, level int) (roleID int64, ok bool, err error) {
|
||||
const q = `
|
||||
SELECT role
|
||||
FROM levelrewards
|
||||
WHERE guild_id = $1 AND levelreq = $2
|
||||
`
|
||||
row := r.db.QueryRowContext(ctx, q, guildID, level)
|
||||
var role sql.NullInt64
|
||||
switch err := row.Scan(&role); err {
|
||||
case nil:
|
||||
if !role.Valid {
|
||||
return 0, false, nil
|
||||
}
|
||||
return role.Int64, true, nil
|
||||
case sql.ErrNoRows:
|
||||
return 0, false, nil
|
||||
default:
|
||||
return 0, false, err
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repo) SetRoleRewardForLevel(ctx context.Context, guildID int64, level int, roleID int64) error {
|
||||
const q = `
|
||||
INSERT INTO levelrewards (guild_id, levelreq, role)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (guild_id, levelreq)
|
||||
DO UPDATE SET role = EXCLUDED.role
|
||||
`
|
||||
_, err := r.db.ExecContext(ctx, q, guildID, level, roleID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repo) SetLevelUpChannel(ctx context.Context, guildID int64, channelID int64) error {
|
||||
const q = `
|
||||
INSERT INTO levelup (guild_id, levelup_channel_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (guild_id)
|
||||
DO UPDATE SET levelup_channel_id = EXCLUDED.levelup_channel_id
|
||||
`
|
||||
_, err := r.db.ExecContext(ctx, q, guildID, channelID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repo) SetLevelUpMessage(ctx context.Context, guildID int64, message string) error {
|
||||
const q = `
|
||||
UPDATE levelsettings
|
||||
SET message = $1
|
||||
WHERE guild_id = $2
|
||||
`
|
||||
_, err := r.db.ExecContext(ctx, q, message, guildID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repo) GetLevelUpMessage(ctx context.Context, guildID int64) (string, error) {
|
||||
const q = `
|
||||
SELECT message
|
||||
FROM levelsettings
|
||||
WHERE guild_id = $1
|
||||
`
|
||||
row := r.db.QueryRowContext(ctx, q, guildID)
|
||||
var message sql.NullString
|
||||
switch err := row.Scan(&message); err {
|
||||
case nil:
|
||||
if !message.Valid {
|
||||
return "GG {user}, you reached level {level}!", nil
|
||||
}
|
||||
return message.String, nil
|
||||
case sql.ErrNoRows:
|
||||
return "GG {user}, you reached level {level}!", nil
|
||||
default:
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package level
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"velox-bot/internal/db/repos/levelrepo"
|
||||
)
|
||||
|
||||
type SettingsProvider interface {
|
||||
IsLevelSystemEnabled(ctx context.Context, guildID int64) (bool, error)
|
||||
GetLevelupChannelID(ctx context.Context, guildID int64) (int64, bool, error)
|
||||
GetRoleRewardForLevel(ctx context.Context, guildID int64, level int) (roleID int64, ok bool, err error)
|
||||
SetLevelUpChannel(ctx context.Context, guildID int64, channelID int64) error
|
||||
SetLevelUpMessage(ctx context.Context, guildID int64, message string) error
|
||||
GetLevelUpMessage(ctx context.Context, guildID int64) (string, error)
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
LeveledUp bool
|
||||
NewLevel int
|
||||
RoleRewardID int64 // 0 if none
|
||||
LevelupMessage string // you can fill this later
|
||||
LevelupChannel int64 // 0 means "use current channel"
|
||||
UpdatedXP int64
|
||||
UpdatedLevel int
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
levels *levelrepo.Repo
|
||||
settings SettingsProvider
|
||||
rand *rand.Rand
|
||||
}
|
||||
|
||||
func New(levels *levelrepo.Repo, settings SettingsProvider) *Service {
|
||||
return &Service{
|
||||
levels: levels,
|
||||
settings: settings,
|
||||
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
}
|
||||
}
|
||||
|
||||
// HandleMessage mirrors your Python gain_xp behavior, minus Discord specifics.
|
||||
func (s *Service) HandleMessage(ctx context.Context, guildID, userID int64) (*Result, error) {
|
||||
enabled, err := s.settings.IsLevelSystemEnabled(ctx, guildID)
|
||||
if err != nil || !enabled {
|
||||
return &Result{LeveledUp: false}, err
|
||||
}
|
||||
|
||||
lvl, err := s.levels.GetLevel(ctx, guildID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// XP gain rules
|
||||
shouldGainXP := false
|
||||
if lvl.Level < 5 {
|
||||
shouldGainXP = true
|
||||
} else {
|
||||
// random.randint(1, level//4) == 1
|
||||
div := lvl.Level / 4
|
||||
if div < 1 {
|
||||
div = 1
|
||||
}
|
||||
if s.rand.Intn(div)+1 == 1 {
|
||||
shouldGainXP = true
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldGainXP {
|
||||
return &Result{
|
||||
LeveledUp: false,
|
||||
UpdatedXP: lvl.XP,
|
||||
UpdatedLevel: lvl.Level,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// This assumes your setXp() adds some fixed XP per hit; adapt amount as needed.
|
||||
xpPerMessage := s.rand.Intn(3) + 1
|
||||
lvl.XP += int64(xpPerMessage)
|
||||
|
||||
res := &Result{
|
||||
LeveledUp: false,
|
||||
UpdatedXP: lvl.XP,
|
||||
UpdatedLevel: lvl.Level,
|
||||
}
|
||||
|
||||
// Level-up rule: if xp >= 100 -> level++
|
||||
if lvl.XP >= 100 {
|
||||
lvl.Level++
|
||||
lvl.XP = 0 // or keep overflow, depending on Python behavior
|
||||
|
||||
res.LeveledUp = true
|
||||
res.NewLevel = lvl.Level
|
||||
res.UpdatedXP = lvl.XP
|
||||
res.UpdatedLevel = lvl.Level
|
||||
|
||||
roleID, ok, err := s.settings.GetRoleRewardForLevel(ctx, guildID, lvl.Level)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
res.RoleRewardID = roleID
|
||||
}
|
||||
|
||||
chID, ok, err := s.settings.GetLevelupChannelID(ctx, guildID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
res.LevelupChannel = chID
|
||||
}
|
||||
}
|
||||
|
||||
// Persist
|
||||
if err := s.levels.UpsertLevel(ctx, lvl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) SetLevel(ctx context.Context, guildID, userID int64, level int) error {
|
||||
return s.levels.SetLevel(ctx, guildID, userID, level)
|
||||
}
|
||||
|
||||
func (s *Service) GetLevel(ctx context.Context, guildID, userID int64) (*levelrepo.Level, error) {
|
||||
return s.levels.GetLevel(ctx, guildID, userID)
|
||||
}
|
||||
|
||||
func (s *Service) TopLevels(ctx context.Context, guildID int64, limit int) ([]*levelrepo.Level, error) {
|
||||
return s.levels.TopLevels(ctx, guildID, limit)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package levelsettings
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"velox-bot/internal/db/repos/settingsrepo"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
settings *settingsrepo.Repo
|
||||
}
|
||||
|
||||
func New(settings *settingsrepo.Repo) *Service {
|
||||
return &Service{settings: settings}
|
||||
}
|
||||
|
||||
func (s *Service) ToggleLevelSystem(ctx context.Context, guildID int64) (bool, error) {
|
||||
enabled, err := s.settings.IsLevelSystemEnabled(ctx, guildID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
newEnabled := !enabled
|
||||
if err := s.settings.SetLevelSystemEnabled(ctx, guildID, newEnabled); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return newEnabled, nil
|
||||
}
|
||||
|
||||
func (s *Service) SetLevelSystemEnabled(ctx context.Context, guildID int64, enabled bool) error {
|
||||
return s.settings.SetLevelSystemEnabled(ctx, guildID, enabled)
|
||||
}
|
||||
|
||||
func (s *Service) SetLevelUpChannel(ctx context.Context, guildID int64, channelID int64) error {
|
||||
return s.settings.SetLevelUpChannel(ctx, guildID, channelID)
|
||||
}
|
||||
|
||||
func (s *Service) SetLevelUpMessage(ctx context.Context, guildID int64, message string) error {
|
||||
return s.settings.SetLevelUpMessage(ctx, guildID, message)
|
||||
}
|
||||
|
||||
func (s *Service) GetLevelUpMessage(ctx context.Context, guildID int64) (string, error) {
|
||||
return s.settings.GetLevelUpMessage(ctx, guildID)
|
||||
}
|
||||
|
||||
func (s *Service) SetRoleRewardForLevel(ctx context.Context, guildID int64, level int, roleID int64) error {
|
||||
return s.settings.SetRoleRewardForLevel(ctx, guildID, level, roleID)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"velox-bot/internal/db/services/level"
|
||||
"velox-bot/internal/db/services/levelsettings"
|
||||
)
|
||||
|
||||
type Services struct {
|
||||
Level *level.Service
|
||||
LevelSettings *levelsettings.Service
|
||||
}
|
||||
|
||||
var Global *Services
|
||||
|
||||
func NewServices(level *level.Service, levelSettings *levelsettings.Service) *Services {
|
||||
s := &Services{
|
||||
Level: level,
|
||||
LevelSettings: levelSettings,
|
||||
}
|
||||
Global = s
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user