feat: add cards templete
This commit is contained in:
@@ -50,14 +50,16 @@ type VIPPackage struct {
|
||||
}
|
||||
|
||||
// TableTier نوع میز: تعداد دست، ورودی، جایزه، XP و جام.
|
||||
// RankReward > 0 یعنی میزِ رتبهبندی است (به برنده امتیازِ رتبه میدهد).
|
||||
type TableTier struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Hands int `json:"hands"`
|
||||
Entry int64 `json:"entry"`
|
||||
Prize int64 `json:"prize"`
|
||||
XP int `json:"xp"`
|
||||
Trophy int `json:"trophy"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Hands int `json:"hands"`
|
||||
Entry int64 `json:"entry"`
|
||||
Prize int64 `json:"prize"`
|
||||
XP int `json:"xp"`
|
||||
Trophy int `json:"trophy"`
|
||||
RankReward int `json:"rank_reward"`
|
||||
}
|
||||
|
||||
// Catalog کل کاتالوگ فروشگاه (برای GET /api/shop). از دیتابیس بارگذاری میشود.
|
||||
@@ -70,6 +72,29 @@ type Catalog struct {
|
||||
TableTiers []TableTier `json:"table_tiers"`
|
||||
}
|
||||
|
||||
// rankTiers آستانهی امتیازِ هر نشانِ رتبه (از پایین به بالا).
|
||||
var rankTiers = []struct {
|
||||
Name string
|
||||
Min int64
|
||||
}{
|
||||
{"bronze", 0},
|
||||
{"silver", 500},
|
||||
{"gold", 1500},
|
||||
{"diamond", 3000},
|
||||
{"king", 5000},
|
||||
}
|
||||
|
||||
// RankTier نام و اندیسِ نشانِ رتبه را از امتیازِ رتبه برمیگرداند.
|
||||
func RankTier(points int64) (name string, index int) {
|
||||
name, index = rankTiers[0].Name, 0
|
||||
for i, t := range rankTiers {
|
||||
if points >= t.Min {
|
||||
name, index = t.Name, i
|
||||
}
|
||||
}
|
||||
return name, index
|
||||
}
|
||||
|
||||
// LevelInfo سطح فعلی، XP درونسطح و XP لازم برای سطح بعد را از XP کل محاسبه میکند.
|
||||
// نیاز هر سطح: ۵۰ × شماره سطح (مثلاً سطح ۸ ⇒ ۴۰۰).
|
||||
func LevelInfo(totalXP int64) (level int, into int64, need int64) {
|
||||
|
||||
@@ -83,13 +83,13 @@ func (s *Service) LoadCatalog(ctx context.Context) error {
|
||||
rows.Close()
|
||||
|
||||
rows, err = s.db.QueryContext(ctx,
|
||||
`SELECT id, title, hands, entry, prize, xp, trophy FROM table_tiers WHERE enabled = 1 ORDER BY sort`)
|
||||
`SELECT id, title, hands, entry, prize, xp, trophy, rank_reward FROM table_tiers WHERE enabled = 1 ORDER BY sort`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
var p TableTier
|
||||
if err := rows.Scan(&p.ID, &p.Title, &p.Hands, &p.Entry, &p.Prize, &p.XP, &p.Trophy); err != nil {
|
||||
if err := rows.Scan(&p.ID, &p.Title, &p.Hands, &p.Entry, &p.Prize, &p.XP, &p.Trophy, &p.RankReward); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ type Profile struct {
|
||||
VIP bool `json:"vip"`
|
||||
VIPUntil *string `json:"vip_until"`
|
||||
SelectedCard string `json:"selected_card"`
|
||||
RankPoints int64 `json:"rank_points"`
|
||||
RankTier string `json:"rank_tier"` // bronze..king
|
||||
RankIndex int `json:"rank_index"` // ۰..۴
|
||||
}
|
||||
|
||||
// GetProfile وضعیت اقتصادی کاربر را میخواند.
|
||||
@@ -63,8 +66,8 @@ func (s *Service) GetProfile(ctx context.Context, userID int64) (*Profile, error
|
||||
var p Profile
|
||||
var vipUntil sql.NullString
|
||||
err := s.db.QueryRowContext(ctx,
|
||||
`SELECT coins, tickets, xp, trophies, vip_until, selected_card FROM users WHERE id = ?`, userID).
|
||||
Scan(&p.Coins, &p.Tickets, &p.XP, &p.Trophies, &vipUntil, &p.SelectedCard)
|
||||
`SELECT coins, tickets, xp, trophies, vip_until, selected_card, rank_points FROM users WHERE id = ?`, userID).
|
||||
Scan(&p.Coins, &p.Tickets, &p.XP, &p.Trophies, &vipUntil, &p.SelectedCard, &p.RankPoints)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
@@ -72,6 +75,7 @@ func (s *Service) GetProfile(ctx context.Context, userID int64) (*Profile, error
|
||||
return nil, err
|
||||
}
|
||||
p.Level, p.XPInto, p.XPNeed = LevelInfo(p.XP)
|
||||
p.RankTier, p.RankIndex = RankTier(p.RankPoints)
|
||||
if vipUntil.Valid {
|
||||
p.VIPUntil = &vipUntil.String
|
||||
p.VIP = parseTS(vipUntil.String).After(nowUTC())
|
||||
|
||||
@@ -176,6 +176,23 @@ func (h *Handler) TablesInfo(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// Leaderboard — GET /api/leaderboard (جدولِ رتبهبندیِ فصل)
|
||||
func (h *Handler) Leaderboard(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := uid(r); !ok {
|
||||
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
list, err := h.svc.Leaderboard(r.Context(), 50)
|
||||
if err != nil {
|
||||
httpx.Error(w, http.StatusInternalServerError, "server error")
|
||||
return
|
||||
}
|
||||
httpx.JSON(w, http.StatusOK, map[string]any{
|
||||
"season": h.svc.Season(r.Context()),
|
||||
"entries": list,
|
||||
})
|
||||
}
|
||||
|
||||
// Daily — POST /api/rewards/daily
|
||||
func (h *Handler) Daily(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := uid(r)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package economy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// RankEntry یک ردیف از جدولِ رتبهبندی.
|
||||
type RankEntry struct {
|
||||
Rank int `json:"rank"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
RankPoints int64 `json:"rank_points"`
|
||||
Tier string `json:"tier"`
|
||||
}
|
||||
|
||||
// AwardRank امتیازِ رتبه را پس از یک بازیِ رتبهبندی بهروزرسانی میکند.
|
||||
// برنده +reward، بازنده −reward/2 (کفِ صفر). برای میزِ غیرِرتبهبندی بیاثر است.
|
||||
func (s *Service) AwardRank(ctx context.Context, userID int64, reward int, won bool) error {
|
||||
if reward <= 0 {
|
||||
return nil
|
||||
}
|
||||
delta := reward
|
||||
if !won {
|
||||
delta = -(reward / 2)
|
||||
}
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
`UPDATE users SET rank_points = MAX(0, rank_points + ?) WHERE id = ?`,
|
||||
delta, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// Leaderboard فهرستِ برترین بازیکنان بر اساس امتیازِ رتبه را برمیگرداند.
|
||||
func (s *Service) Leaderboard(ctx context.Context, limit int) ([]RankEntry, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
`SELECT first_name, avatar, rank_points FROM users
|
||||
WHERE rank_points > 0 ORDER BY rank_points DESC LIMIT ?`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]RankEntry, 0, limit)
|
||||
rank := 0
|
||||
for rows.Next() {
|
||||
var name, avatar sql.NullString
|
||||
var pts int64
|
||||
if err := rows.Scan(&name, &avatar, &pts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rank++
|
||||
tier, _ := RankTier(pts)
|
||||
nm := name.String
|
||||
if nm == "" {
|
||||
nm = "بازیکن"
|
||||
}
|
||||
out = append(out, RankEntry{
|
||||
Rank: rank, Name: nm, Avatar: avatar.String, RankPoints: pts, Tier: tier,
|
||||
})
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ResetSeason امتیازِ رتبهی همهی کاربران را صفر کرده و شمارهی فصل را زیاد میکند.
|
||||
func (s *Service) ResetSeason(ctx context.Context) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE users SET rank_points = 0`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`UPDATE app_settings SET value = value + 1 WHERE key = 'season'`); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Season شمارهی فصلِ جاری را برمیگرداند.
|
||||
func (s *Service) Season(ctx context.Context) int {
|
||||
var v int
|
||||
_ = s.db.QueryRowContext(ctx, `SELECT value FROM app_settings WHERE key = 'season'`).Scan(&v)
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user