package economy import "context" // Avatar یک شخصیتِ آواتار (کازمتیک تصویری) که در کلِ بازی دیده می‌شود. تصویرِ آن // از /avatars/.png سرو می‌شود. Owned یعنی قابلِ انتخاب. type Avatar struct { ID string `json:"id"` Title string `json:"title"` PriceCoins int64 `json:"price_coins"` VIP bool `json:"vip"` Img string `json:"img"` // URL کاملِ تصویرِ شخصیت (URL-safe) Owned bool `json:"owned"` } // Avatars آواتارهای فعال را با وضعیتِ مالکیتِ کاربر برمی‌گرداند. func (s *Service) Avatars(ctx context.Context, userID int64) ([]Avatar, error) { rows, err := s.db.QueryContext(ctx, `SELECT id, title, price_coins, vip, img_ext FROM avatars WHERE enabled = 1 ORDER BY sort`) if err != nil { return nil, err } defer rows.Close() var avatars []Avatar for rows.Next() { var a Avatar var vip int var ext string if err := rows.Scan(&a.ID, &a.Title, &a.PriceCoins, &vip, &ext); err != nil { return nil, err } a.VIP = vip == 1 a.Img = s.imgURL("avatars", a.ID+"."+ext) // URL کاملِ URL-safe avatars = append(avatars, a) } if err := rows.Err(); err != nil { return nil, err } owned, err := s.ownedAvatars(ctx, userID) if err != nil { return nil, err } isVIP := s.isVIP(ctx, userID) for i := range avatars { a := &avatars[i] a.Owned = (a.PriceCoins == 0 && !a.VIP) || (a.VIP && isVIP) || owned[a.ID] } return avatars, nil } func (s *Service) ownedAvatars(ctx context.Context, userID int64) (map[string]bool, error) { rows, err := s.db.QueryContext(ctx, `SELECT avatar_id FROM user_avatars WHERE user_id = ?`, userID) if err != nil { return nil, err } defer rows.Close() owned := map[string]bool{} for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } owned[id] = true } return owned, rows.Err() } // BuyAvatar یک شخصیتِ آواتار را با سکه باز می‌کند. func (s *Service) BuyAvatar(ctx context.Context, userID int64, avatarID string) error { var price int64 var vip, enabled int err := s.db.QueryRowContext(ctx, `SELECT price_coins, vip, enabled FROM avatars WHERE id = ?`, avatarID). Scan(&price, &vip, &enabled) if err != nil || enabled != 1 { return ErrNotFound } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() var exists int _ = tx.QueryRowContext(ctx, `SELECT 1 FROM user_avatars WHERE user_id = ? AND avatar_id = ?`, userID, avatarID).Scan(&exists) if exists == 1 { return ErrAlreadyOwned } if price > 0 { if err := adjustTx(ctx, tx, userID, CurrencyCoin, -price, "buy_avatar", avatarID); err != nil { return err } } if _, err := tx.ExecContext(ctx, `INSERT INTO user_avatars (user_id, avatar_id) VALUES (?, ?)`, userID, avatarID); err != nil { return err } // خرید = مالکیت + انتخابِ خودکار. if _, err := tx.ExecContext(ctx, `UPDATE users SET selected_avatar = ? WHERE id = ?`, avatarID, userID); err != nil { return err } return tx.Commit() } // SelectAvatar شخصیتِ انتخابیِ کاربر را تنظیم می‌کند (خالی ⇒ بازگشت به آواتارِ // تصادفی؛ وگرنه باید مالکش باشد یا رایگان/VIP). func (s *Service) SelectAvatar(ctx context.Context, userID int64, avatarID string) error { if avatarID == "" { _, err := s.db.ExecContext(ctx, `UPDATE users SET selected_avatar = '' WHERE id = ?`, userID) return err } avatars, err := s.Avatars(ctx, userID) if err != nil { return err } for _, a := range avatars { if a.ID == avatarID { if !a.Owned { return ErrNotOwned } _, err := s.db.ExecContext(ctx, `UPDATE users SET selected_avatar = ? WHERE id = ?`, avatarID, userID) return err } } return ErrNotFound }