feat: add characters

This commit is contained in:
2026-07-10 00:56:03 +03:30
parent da6df0328f
commit fe0b63083c
19 changed files with 635 additions and 92 deletions
+28 -3
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"net/url"
"strings"
"sync"
"time"
@@ -36,6 +37,8 @@ type Service struct {
ad AdVerifier
iap IAPVerifier
publicURL string // مبدأِ عمومیِ سرور برای ساختِ URL کاملِ تصاویر
mu sync.RWMutex // محافظ کشِ کاتالوگ
cat Catalog // کشِ کاتالوگ خوانده‌شده از DB
}
@@ -44,6 +47,18 @@ func New(db *sql.DB, ad AdVerifier, iap IAPVerifier) *Service {
return &Service{db: db, ad: ad, iap: iap}
}
// SetPublicURL مبدأِ عمومیِ سرور را تنظیم می‌کند (برای URLهای کاملِ تصاویر).
func (s *Service) SetPublicURL(u string) { s.publicURL = strings.TrimRight(u, "/") }
// imgURL یک URLِ کاملِ URL-safe برای فایلی در پوشه‌ی [dir] می‌سازد ("" اگر نام خالی
// باشد). نامِ فایل encode می‌شود تا فاصله/کاراکترِ خاص مشکلی ایجاد نکند.
func (s *Service) imgURL(dir, name string) string {
if name == "" {
return ""
}
return s.publicURL + "/" + dir + "/" + url.PathEscape(name)
}
// Profile وضعیت اقتصادی کاربر.
type Profile struct {
Coins int64 `json:"coins"`
@@ -58,6 +73,9 @@ type Profile struct {
SelectedCard string `json:"selected_card"`
SelectedCarpet string `json:"selected_carpet"`
SelectedFrame string `json:"selected_frame"`
SelectedAvatar string `json:"selected_avatar"` // شناسه‌ی شخصیتِ انتخابی (برای وضعیتِ انتخاب در فروشگاه)
SelectedAvatarImg string `json:"selected_avatar_img"` // URL کاملِ تصویرِ شخصیت — خالی ⇒ آواتارِ تصادفی
SelectedCarpetImg string `json:"selected_carpet_img"` // URL کاملِ تصویرِ فرش — خالی ⇒ میزِ کلاسیک
RankPoints int64 `json:"rank_points"`
RankTier string `json:"rank_tier"` // bronze..king
RankIndex int `json:"rank_index"` // ۰..۴
@@ -71,10 +89,12 @@ type Profile struct {
// GetProfile وضعیت اقتصادی کاربر را می‌خواند.
func (s *Service) GetProfile(ctx context.Context, userID int64) (*Profile, error) {
var p Profile
var vipUntil, avatar, lastDaily sql.NullString
var vipUntil, avatar, lastDaily, avatarImg sql.NullString
err := s.db.QueryRowContext(ctx,
`SELECT coins, tickets, xp, trophies, vip_until, selected_card, selected_carpet, selected_frame, rank_points, avatar, daily_streak, best_streak, last_daily_at FROM users WHERE id = ?`, userID).
Scan(&p.Coins, &p.Tickets, &p.XP, &p.Trophies, &vipUntil, &p.SelectedCard, &p.SelectedCarpet, &p.SelectedFrame, &p.RankPoints, &avatar, &p.DailyStreak, &p.BestStreak, &lastDaily)
`SELECT coins, tickets, xp, trophies, vip_until, selected_card, selected_carpet, selected_frame, selected_avatar,
(SELECT id || '.' || img_ext FROM avatars WHERE id = users.selected_avatar AND enabled = 1),
rank_points, avatar, daily_streak, best_streak, last_daily_at FROM users WHERE id = ?`, userID).
Scan(&p.Coins, &p.Tickets, &p.XP, &p.Trophies, &vipUntil, &p.SelectedCard, &p.SelectedCarpet, &p.SelectedFrame, &p.SelectedAvatar, &avatarImg, &p.RankPoints, &avatar, &p.DailyStreak, &p.BestStreak, &lastDaily)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
@@ -82,6 +102,11 @@ func (s *Service) GetProfile(ctx context.Context, userID int64) (*Profile, error
return nil, err
}
p.Avatar = avatar.String
p.SelectedAvatarImg = s.imgURL("avatars", avatarImg.String)
// فرش: 'classic' تصویری ندارد (میزِ ترسیمی)؛ بقیه از /carpets/<id>.jpg.
if p.SelectedCarpet != "" && p.SelectedCarpet != "classic" {
p.SelectedCarpetImg = s.imgURL("carpets", p.SelectedCarpet+".jpg")
}
p.Level, p.XPInto, p.XPNeed = LevelInfo(p.XP)
p.RankTier, p.RankIndex = RankTier(p.RankPoints)
if vipUntil.Valid {