Skip to content

Commit

Permalink
feat(leveling): Enhance level command to generate and return user lev…
Browse files Browse the repository at this point in the history
…el images
  • Loading branch information
Paranoia8972 committed Dec 20, 2024
1 parent d119f6f commit 9e8f936
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 22 deletions.
38 changes: 16 additions & 22 deletions internal/pkg/commands/leveling.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"log"
"strconv"

"github.com/Paranoia8972/PixelBot/internal/db"
"github.com/Paranoia8972/PixelBot/internal/pkg/utils"
Expand All @@ -21,33 +20,28 @@ func LevelCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
} else {
user = i.Member.User
}
guildID := i.GuildID
userID := user.ID

currentXP, currentLevel := utils.GetUserXPLevel(guildID, userID)
xpNeeded := utils.CalculateXPNeeded(currentLevel)

embed := &discordgo.MessageEmbed{
Title: fmt.Sprintf("Level for %s", user.Username),
Fields: []*discordgo.MessageEmbedField{
{
Name: "Level",
Value: strconv.Itoa(currentLevel),
Inline: true,
},
{
Name: "XP",
Value: fmt.Sprintf("%d/%d", currentXP, xpNeeded),
Inline: true,

imgBuf, err := utils.GenerateLevelImage(s, user, i.GuildID)
if err != nil {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Failed to generate level card.",
Flags: 64,
},
},
Color: 0x00FF00,
})
return
}

s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{embed},
Files: []*discordgo.File{
{
Name: "level.png",
Reader: imgBuf,
},
},
},
})
}
Expand Down
163 changes: 163 additions & 0 deletions internal/pkg/utils/leveling.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package utils

import (
"bytes"
"context"
"fmt"
"image"
"log"
"net/http"

"github.com/Paranoia8972/PixelBot/internal/db"
"github.com/bwmarrin/discordgo"
"github.com/fogleman/gg"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
Expand Down Expand Up @@ -173,3 +177,162 @@ func GetChannelRequirement(guildID, channelID string) int {
}
return result.RequiredLevel
}

func GetUserRank(guildID, userID string, weekly bool) (int, int) {
collection := "levels"
if weekly {
collection = "weekly_levels"
}

pipeline := []bson.M{
{
"$match": bson.M{
"guild_id": guildID,
},
},
{
"$sort": bson.M{
"xp": -1,
},
},
}

cursor, err := db.GetCollection(cfg.DBName, collection).Aggregate(context.TODO(), pipeline)
if err != nil {
return 0, 0
}
defer cursor.Close(context.TODO())

rank := 1
totalUsers := 0
for cursor.Next(context.TODO()) {
var result struct {
UserID string `bson:"user_id"`
}
if err := cursor.Decode(&result); err != nil {
continue
}
if result.UserID == userID {
return rank, totalUsers + 1
}
rank++
totalUsers++
}
return 0, totalUsers + 1
}

func GetWeeklyXP(guildID, userID string) int {
var result struct {
XP int `bson:"xp"`
}
err := db.GetCollection(cfg.DBName, "weekly_levels").FindOne(context.TODO(), bson.M{
"guild_id": guildID,
"user_id": userID,
}).Decode(&result)
if err != nil {
return 0
}
return result.XP
}

func GenerateLevelImage(s *discordgo.Session, user *discordgo.User, guildID string) (*bytes.Buffer, error) {
const width = 800
const height = 200

dc := gg.NewContext(width, height)

dc.SetRGBA(0, 0, 0, 0)
dc.Clear()

// Left block - Profile section
dc.SetRGB(0.15, 0.15, 0.15)
dc.DrawRoundedRectangle(20, 20, 560, 160, 10)
dc.Fill()

// Right top block - Level
dc.DrawRoundedRectangle(600, 20, 180, 75, 10)
dc.Fill()

// Right bottom block - XP
dc.DrawRoundedRectangle(600, 105, 180, 75, 10)
dc.Fill()

// Get user avatar
resp, err := http.Get(user.AvatarURL("128"))
if err != nil {
return nil, err
}
defer resp.Body.Close()

avatar, _, err := image.Decode(resp.Body)
if err != nil {
return nil, err
}

// Avatar circle
dc.DrawCircle(100, 100, 50)
dc.Clip()
dc.DrawImage(avatar, 50, 50)
dc.ResetClip()

xp, level := GetUserXPLevel(guildID, user.ID)
weeklyXP := GetWeeklyXP(guildID, user.ID)
totalRank, totalUsers := GetUserRank(guildID, user.ID, false)
weeklyRank, _ := GetUserRank(guildID, user.ID, true)
xpNeeded := CalculateXPNeeded(level)

// Text for username
dc.SetRGB(1, 1, 1)
dc.LoadFontFace("assets/Jersey20-Regular.ttf", 32)
dc.DrawString(user.Username, 170, 60)

// Stats labels
dc.SetRGB(0.6, 0.6, 0.6)
dc.LoadFontFace("assets/Jersey20-Regular.ttf", 24)
dc.DrawString("Server Rank", 170, 90)
dc.DrawString("Weekly Rank", 300, 90)
dc.DrawString("Weekly XP", 430, 90)

// Stats values
dc.LoadFontFace("assets/Jersey20-Regular.ttf", 30)

// Server rank
dc.SetRGB(0.4, 0.8, 1.0)
dc.DrawStringAnchored(fmt.Sprintf("%d/%d", totalRank, totalUsers), 215, 120, 0.5, 0)

// Weekly stats
dc.SetRGB(1, 1, 1)
dc.DrawStringAnchored(fmt.Sprintf("%d", weeklyRank), 345, 120, 0.5, 0)
dc.DrawStringAnchored(fmt.Sprintf("%d", weeklyXP), 475, 120, 0.5, 0)

// Level in right top block
dc.LoadFontFace("assets/Jersey20-Regular.ttf", 30)
dc.DrawStringAnchored(fmt.Sprintf("Level %d", level), 690, 60, 0.5, 0.5)

// XP in right bottom block
dc.SetRGB(1, 1, 1)
dc.DrawStringAnchored(fmt.Sprintf("%d/%d XP", xp, xpNeeded), 690, 145, 0.5, 0.5)

// XP Progress bar
barWidth := 560
barHeight := 10
progress := float64(xp) / float64(xpNeeded)

// Background bar
dc.SetRGB(0.2, 0.2, 0.2)
dc.DrawRoundedRectangle(20, 170, float64(barWidth), float64(barHeight), 5)
dc.Fill()

// Progress bar
dc.SetRGB(0.4, 0.4, 1.0)
dc.DrawRoundedRectangle(20, 170, float64(barWidth)*progress, float64(barHeight), 5)
dc.Fill()

buf := new(bytes.Buffer)
err = dc.EncodePNG(buf)
if err != nil {
return nil, err
}

return buf, nil
}

0 comments on commit 9e8f936

Please sign in to comment.