Files
dodep-simulator/achievements.py
T
2026-07-05 17:40:52 +00:00

2605 lines
92 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from typing import Optional
from sqlalchemy.orm import Session
from database import Achievement, UserAchievement, ActivityFeed
from datetime import datetime
import json
import math
# ─── Определения достижений ─────────────────────────────────────────────────
ACHIEVEMENT_DEFS = [
# ── Существующие (сохраняем порядок) ─────────────────────────────────────
{
"name": "first_case",
"title": "Первый шаг",
"description": "Открой свой первый кейс",
"icon": "📦",
"category": "cases",
"requirement_type": "open_cases",
"requirement_value": 1,
"reward_amount": 100,
"sort_order": 1,
},
{
"name": "case_collector_10",
"title": "Коллекционер",
"description": "Открой 10 кейсов",
"icon": "📦",
"category": "cases",
"requirement_type": "open_cases",
"requirement_value": 10,
"reward_amount": 250,
"sort_order": 2,
},
{
"name": "case_collector_100",
"title": "Заядлый коллекционер",
"description": "Открой 100 кейсов",
"icon": "📦",
"category": "cases",
"requirement_type": "open_cases",
"requirement_value": 100,
"reward_amount": 1000,
"sort_order": 3,
},
{
"name": "case_collector_1000",
"title": "Одержимый",
"description": "Открой 1000 кейсов",
"icon": "💎",
"category": "cases",
"requirement_type": "open_cases",
"requirement_value": 1000,
"reward_amount": 5000,
"sort_order": 4,
},
{
"name": "first_contract",
"title": "Обменщик",
"description": "Выполни первый контракт обмена",
"icon": "🔄",
"category": "contracts",
"requirement_type": "contracts",
"requirement_value": 1,
"reward_amount": 200,
"sort_order": 5,
},
{
"name": "contract_master",
"title": "Мастер контрактов",
"description": "Выполни 50 контрактов обмена",
"icon": "🔄",
"category": "contracts",
"requirement_type": "contracts",
"requirement_value": 50,
"reward_amount": 2000,
"sort_order": 6,
},
{
"name": "first_upgrade",
"title": "Рисковый",
"description": "Попробуй апгрейд в первый раз",
"icon": "🆙",
"category": "upgrade",
"requirement_type": "upgrade_attempts",
"requirement_value": 1,
"reward_amount": 100,
"sort_order": 7,
},
{
"name": "upgrade_winner",
"title": "Везунчик",
"description": "Выиграй 10 апгрейдов",
"icon": "🍀",
"category": "upgrade",
"requirement_type": "upgrade_wins",
"requirement_value": 10,
"reward_amount": 1500,
"sort_order": 8,
},
{
"name": "rare_finder",
"title": "Охотник за редким",
"description": "Выбей предмет редкости Covert или Rare Special",
"icon": "🔴",
"category": "cases",
"requirement_type": "rare_item",
"requirement_value": 1,
"reward_amount": 500,
"sort_order": 9,
},
{
"name": "knife_collector",
"title": "Ножевод",
"description": "Выбей нож или перчатки",
"icon": "🔪",
"category": "cases",
"requirement_type": "knife",
"requirement_value": 1,
"reward_amount": 2000,
"sort_order": 10,
},
{
"name": "big_spender",
"title": "Транжира",
"description": "Потрать 10000 ₽ на кейсы",
"icon": "💰",
"category": "general",
"requirement_type": "total_spent",
"requirement_value": 10000,
"reward_amount": 1000,
"sort_order": 11,
},
{
"name": "whale",
"title": "Кит",
"description": "Потрать 100000 ₽ на кейсы",
"icon": "🐋",
"category": "general",
"requirement_type": "total_spent",
"requirement_value": 100000,
"reward_amount": 10000,
"sort_order": 12,
},
{
"name": "slot_winner",
"title": "Джекпот",
"description": "Собери 3 одинаковых символа в слот-машине",
"icon": "🎰",
"category": "cases",
"requirement_type": "slot_match",
"requirement_value": 1,
"reward_amount": 1000,
"sort_order": 13,
},
{
"name": "rich_100k",
"title": "Богач",
"description": "Накопи 100000 ₽ на балансе",
"icon": "🤑",
"category": "general",
"requirement_type": "balance_milestone",
"requirement_value": 100000,
"reward_amount": 5000,
"sort_order": 14,
},
{
"name": "slots_10",
"title": "Слото-фанат",
"description": "Сыграй 10 раундов в слот-машине",
"icon": "🎰",
"category": "cases",
"requirement_type": "slot_plays",
"requirement_value": 10,
"reward_amount": 500,
"sort_order": 15,
},
{
"name": "crash_first",
"title": "Крэш-тестер",
"description": "Сыграй в Crash в первый раз",
"icon": "💥",
"category": "crash",
"requirement_type": "crash_plays",
"requirement_value": 1,
"reward_amount": 100,
"sort_order": 16,
},
{
"name": "crash_pro",
"title": "Крэш-профи",
"description": "Сыграй 50 раундов в Crash",
"icon": "💥",
"category": "crash",
"requirement_type": "crash_plays",
"requirement_value": 50,
"reward_amount": 2000,
"sort_order": 17,
},
{
"name": "crash_5x",
"title": "Смельчак",
"description": "Выведи деньги на множителе x5 или выше в Crash",
"icon": "🚀",
"category": "crash",
"requirement_type": "crash_cashout_milestone",
"requirement_value": 5,
"reward_amount": 1500,
"sort_order": 18,
},
{
"name": "inventory_50",
"title": "Собиратель",
"description": "Собери 50 предметов в инвентаре",
"icon": "🎒",
"category": "general",
"requirement_type": "inventory_count",
"requirement_value": 50,
"reward_amount": 500,
"sort_order": 19,
},
# ═══════════════════════════════════════════════════════════════════════════
# 🆙 АПГРЕЙД (28 новых)
# ═══════════════════════════════════════════════════════════════════════════
{
"name": "upgrade_5",
"title": "Начинающий",
"description": "Сыграй 5 апгрейдов",
"icon": "🆙",
"category": "upgrade",
"requirement_type": "upgrade_attempts",
"requirement_value": 5,
"reward_amount": 200,
"sort_order": 20,
},
{
"name": "upgrade_25",
"title": "Энтузиаст",
"description": "Сыграй 25 апгрейдов",
"icon": "🆙",
"category": "upgrade",
"requirement_type": "upgrade_attempts",
"requirement_value": 25,
"reward_amount": 500,
"sort_order": 21,
},
{
"name": "upgrade_50",
"title": "Мастер апгрейда",
"description": "Сыграй 50 апгрейдов",
"icon": "⬆️",
"category": "upgrade",
"requirement_type": "upgrade_attempts",
"requirement_value": 50,
"reward_amount": 1500,
"sort_order": 22,
},
{
"name": "upgrade_100",
"title": "Профи",
"description": "Сыграй 100 апгрейдов",
"icon": "⬆️",
"category": "upgrade",
"requirement_type": "upgrade_attempts",
"requirement_value": 100,
"reward_amount": 3000,
"sort_order": 23,
},
{
"name": "upgrade_250",
"title": "Ветеран",
"description": "Сыграй 250 апгрейдов",
"icon": "",
"category": "upgrade",
"requirement_type": "upgrade_attempts",
"requirement_value": 250,
"reward_amount": 7500,
"sort_order": 24,
},
{
"name": "upgrade_500",
"title": "Легенда",
"description": "Сыграй 500 апгрейдов",
"icon": "",
"category": "upgrade",
"requirement_type": "upgrade_attempts",
"requirement_value": 500,
"reward_amount": 15000,
"sort_order": 25,
},
{
"name": "upgrade_1000",
"title": "Бессмертный",
"description": "Сыграй 1000 апгрейдов",
"icon": "🏆",
"category": "upgrade",
"requirement_type": "upgrade_attempts",
"requirement_value": 1000,
"reward_amount": 30000,
"sort_order": 26,
},
{
"name": "upgrade_wins_5",
"title": "Удачный старт",
"description": "Выиграй 5 апгрейдов",
"icon": "🍀",
"category": "upgrade",
"requirement_type": "upgrade_wins",
"requirement_value": 5,
"reward_amount": 400,
"sort_order": 27,
},
{
"name": "upgrade_wins_25",
"title": "Счастливчик",
"description": "Выиграй 25 апгрейдов",
"icon": "🍀",
"category": "upgrade",
"requirement_type": "upgrade_wins",
"requirement_value": 25,
"reward_amount": 2000,
"sort_order": 28,
},
{
"name": "upgrade_wins_50",
"title": "Баловень судьбы",
"description": "Выиграй 50 апгрейдов",
"icon": "",
"category": "upgrade",
"requirement_type": "upgrade_wins",
"requirement_value": 50,
"reward_amount": 5000,
"sort_order": 29,
},
{
"name": "upgrade_wins_100",
"title": "Везучий",
"description": "Выиграй 100 апгрейдов",
"icon": "",
"category": "upgrade",
"requirement_type": "upgrade_wins",
"requirement_value": 100,
"reward_amount": 10000,
"sort_order": 30,
},
{
"name": "upgrade_wins_250",
"title": "Невезучий... или нет?",
"description": "Выиграй 250 апгрейдов",
"icon": "🌟",
"category": "upgrade",
"requirement_type": "upgrade_wins",
"requirement_value": 250,
"reward_amount": 25000,
"sort_order": 31,
},
{
"name": "upgrade_high_risk_50k",
"title": "Ол-ин",
"description": "Поставь предмет дороже 50 000 ₽ в апгрейд",
"icon": "💀",
"category": "upgrade",
"requirement_type": "upgrade_high_risk_item",
"requirement_value": 50000,
"reward_amount": 2000,
"sort_order": 32,
},
{
"name": "upgrade_high_risk_500k",
"title": "Сорвиголова",
"description": "Поставь предмет дороже 500 000 ₽ в апгрейд",
"icon": "💀",
"category": "upgrade",
"requirement_type": "upgrade_high_risk_item",
"requirement_value": 500000,
"reward_amount": 15000,
"sort_order": 33,
},
{
"name": "upgrade_high_risk_10m",
"title": "Безумец",
"description": "Поставь предмет дороже 10 000 000 ₽ в апгрейд",
"icon": "☠️",
"category": "upgrade",
"requirement_type": "upgrade_high_risk_item",
"requirement_value": 10000000,
"reward_amount": 50000,
"sort_order": 34,
},
{
"name": "upgrade_2000",
"title": "Риск — дело благородное",
"description": "Сыграй 2000 апгрейдов",
"icon": "🎲",
"category": "upgrade",
"requirement_type": "upgrade_attempts",
"requirement_value": 2000,
"reward_amount": 5000,
"sort_order": 35,
},
{
"name": "upgrade_consecutive_wins_3",
"title": "Хет-трик",
"description": "Выиграй 3 апгрейда подряд",
"icon": "🔥",
"category": "upgrade",
"requirement_type": "upgrade_consecutive_wins",
"requirement_value": 3,
"reward_amount": 1000,
"sort_order": 36,
},
{
"name": "upgrade_consecutive_wins_5",
"title": "Победная серия",
"description": "Выиграй 5 апгрейдов подряд",
"icon": "🔥",
"category": "upgrade",
"requirement_type": "upgrade_consecutive_wins",
"requirement_value": 5,
"reward_amount": 3000,
"sort_order": 37,
},
{
"name": "upgrade_consecutive_wins_10",
"title": "Неостановимый",
"description": "Выиграй 10 апгрейдов подряд",
"icon": "🌋",
"category": "upgrade",
"requirement_type": "upgrade_consecutive_wins",
"requirement_value": 10,
"reward_amount": 10000,
"sort_order": 38,
},
{
"name": "upgrade_consecutive_losses_5",
"title": "Неудачник",
"description": "Проиграй 5 апгрейдов подряд",
"icon": "😅",
"category": "upgrade",
"requirement_type": "upgrade_consecutive_losses",
"requirement_value": 5,
"reward_amount": 500,
"sort_order": 39,
},
{
"name": "upgrade_consecutive_losses_10",
"title": "Жертва системы",
"description": "Проиграй 10 апгрейдов подряд",
"icon": "😭",
"category": "upgrade",
"requirement_type": "upgrade_consecutive_losses",
"requirement_value": 10,
"reward_amount": 1500,
"sort_order": 40,
},
{
"name": "upgrade_from_nothing",
"title": "Из грязи в князи",
"description": "Выиграй апгрейд с предметом дешевле 1000 ₽",
"icon": "🪴",
"category": "upgrade",
"requirement_type": "upgrade_low_value_win",
"requirement_value": 1000,
"reward_amount": 2000,
"sort_order": 41,
},
{
"name": "upgrade_miracle",
"title": "Чудо",
"description": "Выиграй апгрейд с шансом менее 10%",
"icon": "🙏",
"category": "upgrade",
"requirement_type": "upgrade_low_prob_win",
"requirement_value": 10,
"reward_amount": 5000,
"sort_order": 42,
},
{
"name": "upgrade_miracle_5",
"title": "Божественное вмешательство",
"description": "Выиграй 5 апгрейдов с шансом менее 10%",
"icon": "😇",
"category": "upgrade",
"requirement_type": "upgrade_low_prob_win",
"requirement_value": 5,
"reward_amount": 30000,
"sort_order": 43,
},
{
"name": "upgrade_spent_100k",
"title": "Инвестор",
"description": "Суммарная стоимость проигранных предметов в апгрейдах — 100 000 ₽",
"icon": "💸",
"category": "upgrade",
"requirement_type": "upgrade_total_spent_value",
"requirement_value": 100000,
"reward_amount": 2000,
"sort_order": 44,
},
{
"name": "upgrade_spent_1m",
"title": "Крупный инвестор",
"description": "Суммарная стоимость проигранных предметов в апгрейдах — 1 000 000 ₽",
"icon": "💸",
"category": "upgrade",
"requirement_type": "upgrade_total_spent_value",
"requirement_value": 1000000,
"reward_amount": 10000,
"sort_order": 45,
},
{
"name": "upgrade_spent_10m",
"title": "Магнат",
"description": "Суммарная стоимость проигранных предметов в апгрейдах — 10 000 000 ₽",
"icon": "💰",
"category": "upgrade",
"requirement_type": "upgrade_total_spent_value",
"requirement_value": 10000000,
"reward_amount": 50000,
"sort_order": 46,
},
{
"name": "upgrade_spent_100m",
"title": "Олигарх",
"description": "Суммарная стоимость проигранных предметов в апгрейдах — 100 000 000 ₽",
"icon": "💰",
"category": "upgrade",
"requirement_type": "upgrade_total_spent_value",
"requirement_value": 100000000,
"reward_amount": 200000,
"sort_order": 47,
},
# ═══════════════════════════════════════════════════════════════════════════
# 📦 КЕЙСЫ (12 новых)
# ═══════════════════════════════════════════════════════════════════════════
{
"name": "cases_2500",
"title": "Кейсоман",
"description": "Открой 2 500 кейсов",
"icon": "📦",
"category": "cases",
"requirement_type": "open_cases",
"requirement_value": 2500,
"reward_amount": 10000,
"sort_order": 48,
},
{
"name": "cases_5000",
"title": "Индустрия кейсов",
"description": "Открой 5 000 кейсов",
"icon": "📦",
"category": "cases",
"requirement_type": "open_cases",
"requirement_value": 5000,
"reward_amount": 25000,
"sort_order": 49,
},
{
"name": "cases_10000",
"title": "Кейс-магнат",
"description": "Открой 10 000 кейсов",
"icon": "🏭",
"category": "cases",
"requirement_type": "open_cases",
"requirement_value": 10000,
"reward_amount": 75000,
"sort_order": 50,
},
{
"name": "cases_25000",
"title": "Повелитель кейсов",
"description": "Открой 25 000 кейсов",
"icon": "👑",
"category": "cases",
"requirement_type": "open_cases",
"requirement_value": 25000,
"reward_amount": 200000,
"sort_order": 51,
},
{
"name": "slots_25",
"title": "Слот-любитель",
"description": "Сыграй 25 раундов в слот-машине",
"icon": "🎰",
"category": "cases",
"requirement_type": "slot_plays",
"requirement_value": 25,
"reward_amount": 1000,
"sort_order": 52,
},
{
"name": "slots_100",
"title": "Слот-зависимый",
"description": "Сыграй 100 раундов в слот-машине",
"icon": "🎰",
"category": "cases",
"requirement_type": "slot_plays",
"requirement_value": 100,
"reward_amount": 5000,
"sort_order": 53,
},
{
"name": "slots_500",
"title": "Слот-король",
"description": "Сыграй 500 раундов в слот-машине",
"icon": "👑",
"category": "cases",
"requirement_type": "slot_plays",
"requirement_value": 500,
"reward_amount": 25000,
"sort_order": 54,
},
{
"name": "slots_jackpot_5",
"title": "Джекпот-хантер",
"description": "Собери 3 одинаковых символа в слотах 5 раз",
"icon": "🎯",
"category": "cases",
"requirement_type": "slot_match_count",
"requirement_value": 5,
"reward_amount": 10000,
"sort_order": 55,
},
{
"name": "slots_jackpot_25",
"title": "Джекпот-легенда",
"description": "Собери 3 одинаковых символа в слотах 25 раз",
"icon": "🎯",
"category": "cases",
"requirement_type": "slot_match_count",
"requirement_value": 25,
"reward_amount": 50000,
"sort_order": 56,
},
{
"name": "rare_item_10",
"title": "Охотник за редким II",
"description": "Выбей 10 предметов редкости Covert+",
"icon": "🔴",
"category": "cases",
"requirement_type": "rare_item_count",
"requirement_value": 10,
"reward_amount": 25000,
"sort_order": 57,
},
{
"name": "rare_item_50",
"title": "Охотник за редким III",
"description": "Выбей 50 предметов редкости Covert+",
"icon": "🔴",
"category": "cases",
"requirement_type": "rare_item_count",
"requirement_value": 50,
"reward_amount": 100000,
"sort_order": 58,
},
{
"name": "knife_5",
"title": "Ножевод II",
"description": "Выбей 5 ножей или перчаток",
"icon": "🔪",
"category": "cases",
"requirement_type": "knife_count",
"requirement_value": 5,
"reward_amount": 50000,
"sort_order": 59,
},
# ═══════════════════════════════════════════════════════════════════════════
# 🎯 ОБЩИЕ (40 новых)
# ═══════════════════════════════════════════════════════════════════════════
{
"name": "spent_500k",
"title": "Транжира II",
"description": "Потрать 500 000 ₽ на кейсы",
"icon": "💰",
"category": "general",
"requirement_type": "total_spent",
"requirement_value": 500000,
"reward_amount": 5000,
"sort_order": 60,
},
{
"name": "spent_1m",
"title": "Транжира III",
"description": "Потрать 1 000 000 ₽ на кейсы",
"icon": "💰",
"category": "general",
"requirement_type": "total_spent",
"requirement_value": 1000000,
"reward_amount": 15000,
"sort_order": 61,
},
{
"name": "spent_10m",
"title": "Транжира IV",
"description": "Потрать 10 000 000 ₽ на кейсы",
"icon": "💵",
"category": "general",
"requirement_type": "total_spent",
"requirement_value": 10000000,
"reward_amount": 50000,
"sort_order": 62,
},
{
"name": "spent_100m",
"title": "Транжира V",
"description": "Потрать 100 000 000 ₽ на кейсы",
"icon": "💵",
"category": "general",
"requirement_type": "total_spent",
"requirement_value": 100000000,
"reward_amount": 200000,
"sort_order": 63,
},
{
"name": "spent_500m",
"title": "Транжира VI",
"description": "Потрать 500 000 000 ₽ на кейсы",
"icon": "💎",
"category": "general",
"requirement_type": "total_spent",
"requirement_value": 500000000,
"reward_amount": 500000,
"sort_order": 64,
},
{
"name": "spent_1b",
"title": "Транжира VII",
"description": "Потрать 1 000 000 000 ₽ на кейсы",
"icon": "💎",
"category": "general",
"requirement_type": "total_spent",
"requirement_value": 1000000000,
"reward_amount": 1000000,
"sort_order": 65,
},
{
"name": "inventory_100",
"title": "Собиратель II",
"description": "Собери 100 предметов в инвентаре",
"icon": "🎒",
"category": "general",
"requirement_type": "inventory_count",
"requirement_value": 100,
"reward_amount": 1000,
"sort_order": 66,
},
{
"name": "inventory_250",
"title": "Собиратель III",
"description": "Собери 250 предметов в инвентаре",
"icon": "🎒",
"category": "general",
"requirement_type": "inventory_count",
"requirement_value": 250,
"reward_amount": 2500,
"sort_order": 67,
},
{
"name": "inventory_500",
"title": "Собиратель IV",
"description": "Собери 500 предметов в инвентаре",
"icon": "🎒",
"category": "general",
"requirement_type": "inventory_count",
"requirement_value": 500,
"reward_amount": 5000,
"sort_order": 68,
},
{
"name": "inventory_1000",
"title": "Коллекционер",
"description": "Собери 1 000 предметов в инвентаре",
"icon": "🏛️",
"category": "general",
"requirement_type": "inventory_count",
"requirement_value": 1000,
"reward_amount": 10000,
"sort_order": 69,
},
{
"name": "inventory_2500",
"title": "Музей",
"description": "Собери 2 500 предметов в инвентаре",
"icon": "🏛️",
"category": "general",
"requirement_type": "inventory_count",
"requirement_value": 2500,
"reward_amount": 25000,
"sort_order": 70,
},
{
"name": "balance_1m",
"title": "Миллионер",
"description": "Накопи 1 000 000 ₽ на балансе",
"icon": "🤑",
"category": "general",
"requirement_type": "balance_milestone",
"requirement_value": 1000000,
"reward_amount": 10000,
"sort_order": 71,
},
{
"name": "balance_10m",
"title": "Мультимиллионер",
"description": "Накопи 10 000 000 ₽ на балансе",
"icon": "🤑",
"category": "general",
"requirement_type": "balance_milestone",
"requirement_value": 10000000,
"reward_amount": 50000,
"sort_order": 72,
},
{
"name": "balance_100m",
"title": "Богач",
"description": "Накопи 100 000 000 ₽ на балансе",
"icon": "💎",
"category": "general",
"requirement_type": "balance_milestone",
"requirement_value": 100000000,
"reward_amount": 250000,
"sort_order": 73,
},
{
"name": "balance_1b",
"title": "Миллиардер",
"description": "Накопи 1 000 000 000 ₽ на балансе",
"icon": "💎",
"category": "general",
"requirement_type": "balance_milestone",
"requirement_value": 1000000000,
"reward_amount": 1000000,
"sort_order": 74,
},
{
"name": "contracts_10",
"title": "Обменщик II",
"description": "Выполни 10 контрактов обмена",
"icon": "🔄",
"category": "general",
"requirement_type": "contracts",
"requirement_value": 10,
"reward_amount": 500,
"sort_order": 75,
},
{
"name": "contracts_25",
"title": "Обменщик III",
"description": "Выполни 25 контрактов обмена",
"icon": "🔄",
"category": "general",
"requirement_type": "contracts",
"requirement_value": 25,
"reward_amount": 1500,
"sort_order": 76,
},
{
"name": "contracts_100",
"title": "Обменщик IV",
"description": "Выполни 100 контрактов обмена",
"icon": "🔄",
"category": "general",
"requirement_type": "contracts",
"requirement_value": 100,
"reward_amount": 5000,
"sort_order": 77,
},
{
"name": "contracts_250",
"title": "Мастер обмена",
"description": "Выполни 250 контрактов обмена",
"icon": "🔄",
"category": "general",
"requirement_type": "contracts",
"requirement_value": 250,
"reward_amount": 15000,
"sort_order": 78,
},
{
"name": "crash_10",
"title": "Крэш-любитель",
"description": "Сыграй 10 раундов в Crash",
"icon": "💥",
"category": "crash",
"requirement_type": "crash_plays",
"requirement_value": 10,
"reward_amount": 500,
"sort_order": 79,
},
{
"name": "crash_25",
"title": "Крэш-игрок",
"description": "Сыграй 25 раундов в Crash",
"icon": "💥",
"category": "crash",
"requirement_type": "crash_plays",
"requirement_value": 25,
"reward_amount": 1000,
"sort_order": 80,
},
{
"name": "crash_100",
"title": "Крэш-фанат",
"description": "Сыграй 100 раундов в Crash",
"icon": "💥",
"category": "crash",
"requirement_type": "crash_plays",
"requirement_value": 100,
"reward_amount": 5000,
"sort_order": 81,
},
{
"name": "crash_500",
"title": "Крэш-маньяк",
"description": "Сыграй 500 раундов в Crash",
"icon": "💥",
"category": "crash",
"requirement_type": "crash_plays",
"requirement_value": 500,
"reward_amount": 15000,
"sort_order": 82,
},
{
"name": "crash_1000",
"title": "Крэш-легенда",
"description": "Сыграй 1000 раундов в Crash",
"icon": "💥",
"category": "crash",
"requirement_type": "crash_plays",
"requirement_value": 1000,
"reward_amount": 50000,
"sort_order": 83,
},
{
"name": "crash_cashout_10x",
"title": "Смельчак II",
"description": "Выведи деньги на множителе x10 или выше в Crash",
"icon": "🚀",
"category": "crash",
"requirement_type": "crash_cashout_milestone",
"requirement_value": 10,
"reward_amount": 5000,
"sort_order": 84,
},
{
"name": "crash_cashout_25x",
"title": "Смельчак III",
"description": "Выведи деньги на множителе x25 или выше в Crash",
"icon": "🚀",
"category": "crash",
"requirement_type": "crash_cashout_milestone",
"requirement_value": 25,
"reward_amount": 15000,
"sort_order": 85,
},
{
"name": "crash_cashout_50x",
"title": "Смельчак IV",
"description": "Выведи деньги на множителе x50 или выше в Crash",
"icon": "🚀",
"category": "crash",
"requirement_type": "crash_cashout_milestone",
"requirement_value": 50,
"reward_amount": 50000,
"sort_order": 86,
},
{
"name": "crash_cashout_100x",
"title": "Смельчак V",
"description": "Выведи деньги на множителе x100 или выше в Crash",
"icon": "🚀",
"category": "crash",
"requirement_type": "crash_cashout_milestone",
"requirement_value": 100,
"reward_amount": 200000,
"sort_order": 87,
},
{
"name": "items_unboxed_100",
"title": "Распаковщик",
"description": "Открой 100 предметов из кейсов",
"icon": "🎁",
"category": "general",
"requirement_type": "items_unboxed",
"requirement_value": 100,
"reward_amount": 1000,
"sort_order": 88,
},
{
"name": "items_unboxed_1000",
"title": "Распаковщик II",
"description": "Открой 1 000 предметов из кейсов",
"icon": "🎁",
"category": "general",
"requirement_type": "items_unboxed",
"requirement_value": 1000,
"reward_amount": 5000,
"sort_order": 89,
},
{
"name": "items_unboxed_10000",
"title": "Распаковщик III",
"description": "Открой 10 000 предметов из кейсов",
"icon": "🎁",
"category": "general",
"requirement_type": "items_unboxed",
"requirement_value": 10000,
"reward_amount": 25000,
"sort_order": 90,
},
{
"name": "total_profit_100k",
"title": "Первая прибыль",
"description": "Получи 100 000 ₽ чистой прибыли",
"icon": "📈",
"category": "general",
"requirement_type": "total_profit",
"requirement_value": 100000,
"reward_amount": 2000,
"sort_order": 91,
},
{
"name": "total_profit_1m",
"title": "Прибыльный игрок",
"description": "Получи 1 000 000 ₽ чистой прибыли",
"icon": "📈",
"category": "general",
"requirement_type": "total_profit",
"requirement_value": 1000000,
"reward_amount": 10000,
"sort_order": 92,
},
{
"name": "total_profit_10m",
"title": "Бизнесмен",
"description": "Получи 10 000 000 ₽ чистой прибыли",
"icon": "📊",
"category": "general",
"requirement_type": "total_profit",
"requirement_value": 10000000,
"reward_amount": 50000,
"sort_order": 93,
},
{
"name": "total_profit_100m",
"title": "Магнат",
"description": "Получи 100 000 000 ₽ чистой прибыли",
"icon": "📊",
"category": "general",
"requirement_type": "total_profit",
"requirement_value": 100000000,
"reward_amount": 250000,
"sort_order": 94,
},
{
"name": "achievements_10",
"title": "Достигатор",
"description": "Открой 10 достижений",
"icon": "🏆",
"category": "general",
"requirement_type": "achievements_count",
"requirement_value": 10,
"reward_amount": 2000,
"sort_order": 95,
},
{
"name": "achievements_25",
"title": "Достигатор II",
"description": "Открой 25 достижений",
"icon": "🏆",
"category": "general",
"requirement_type": "achievements_count",
"requirement_value": 25,
"reward_amount": 5000,
"sort_order": 96,
},
{
"name": "achievements_50",
"title": "Достигатор III",
"description": "Открой 50 достижений",
"icon": "🏆",
"category": "general",
"requirement_type": "achievements_count",
"requirement_value": 50,
"reward_amount": 15000,
"sort_order": 97,
},
{
"name": "achievements_100",
"title": "Достигатор IV",
"description": "Открой 100 достижений",
"icon": "🏆",
"category": "general",
"requirement_type": "achievements_count",
"requirement_value": 100,
"reward_amount": 50000,
"sort_order": 98,
},
{
"name": "crash_5000",
"title": "Крэш-бог",
"description": "Сыграй 5000 раундов в Crash",
"icon": "💥",
"category": "crash",
"requirement_type": "crash_plays",
"requirement_value": 5000,
"reward_amount": 200000,
"sort_order": 99,
},
# ═══════════════════════════════════════════════════════════════════════════
# 🔞 ДИЛДО / ХАРД (20 новых)
# ═══════════════════════════════════════════════════════════════════════════
{
"name": "dildo_owned",
"title": "Интересная находка",
"description": "Получи свой первый Dildo",
"icon": "🔞",
"category": "general",
"requirement_type": "dildo_count",
"requirement_value": 1,
"reward_amount": 5000,
"sort_order": 100,
},
{
"name": "dildo_5",
"title": "Коллекционер Dildo",
"description": "Собери 5 Dildo в инвентаре",
"icon": "🔞",
"category": "general",
"requirement_type": "dildo_count",
"requirement_value": 5,
"reward_amount": 15000,
"sort_order": 101,
},
{
"name": "dildo_10",
"title": "Знаток Dildo",
"description": "Собери 10 Dildo в инвентаре",
"icon": "🔞",
"category": "general",
"requirement_type": "dildo_count",
"requirement_value": 10,
"reward_amount": 30000,
"sort_order": 102,
},
{
"name": "dildo_25",
"title": "Dildo-маньяк",
"description": "Собери 25 Dildo в инвентаре",
"icon": "🔞",
"category": "general",
"requirement_type": "dildo_count",
"requirement_value": 25,
"reward_amount": 75000,
"sort_order": 103,
},
{
"name": "dildo_50",
"title": "Dildo-барон",
"description": "Собери 50 Dildo в инвентаре",
"icon": "🔞",
"category": "general",
"requirement_type": "dildo_count",
"requirement_value": 50,
"reward_amount": 200000,
"sort_order": 104,
},
{
"name": "dildo_100",
"title": "Dildo-император",
"description": "Собери 100 Dildo в инвентаре",
"icon": "👑",
"category": "general",
"requirement_type": "dildo_count",
"requirement_value": 100,
"reward_amount": 500000,
"sort_order": 105,
},
{
"name": "item_worth_1m",
"title": "Ценная вещица",
"description": "Владей предметом дороже 1 000 000 ₽",
"icon": "💎",
"category": "general",
"requirement_type": "best_item_price",
"requirement_value": 1000000,
"reward_amount": 10000,
"sort_order": 106,
},
{
"name": "item_worth_10m",
"title": "Дорогая игрушка",
"description": "Владей предметом дороже 10 000 000 ₽",
"icon": "💎",
"category": "general",
"requirement_type": "best_item_price",
"requirement_value": 10000000,
"reward_amount": 50000,
"sort_order": 107,
},
{
"name": "item_worth_100m",
"title": "Бесценно",
"description": "Владей предметом дороже 100 000 000 ₽",
"icon": "💎",
"category": "general",
"requirement_type": "best_item_price",
"requirement_value": 100000000,
"reward_amount": 250000,
"sort_order": 108,
},
{
"name": "item_worth_1b",
"title": "Священный Грааль",
"description": "Владей предметом дороже 1 000 000 000 ₽",
"icon": "👑",
"category": "general",
"requirement_type": "best_item_price",
"requirement_value": 1000000000,
"reward_amount": 2000000,
"sort_order": 109,
},
{
"name": "inventory_value_10m",
"title": "Инвентарь миллионера",
"description": "Общая стоимость инвентаря > 10 000 000 ₽",
"icon": "💰",
"category": "general",
"requirement_type": "inventory_total_value",
"requirement_value": 10000000,
"reward_amount": 25000,
"sort_order": 110,
},
{
"name": "inventory_value_100m",
"title": "Инвентарь магната",
"description": "Общая стоимость инвентаря > 100 000 000 ₽",
"icon": "💰",
"category": "general",
"requirement_type": "inventory_total_value",
"requirement_value": 100000000,
"reward_amount": 100000,
"sort_order": 111,
},
{
"name": "inventory_value_1b",
"title": "Инвентарь миллиардера",
"description": "Общая стоимость инвентаря > 1 000 000 000 ₽",
"icon": "👑",
"category": "general",
"requirement_type": "inventory_total_value",
"requirement_value": 1000000000,
"reward_amount": 500000,
"sort_order": 112,
},
{
"name": "dildo_float_001",
"title": "Идеальный Dildo",
"description": "Получи Dildo с float < 0.01",
"icon": "",
"category": "general",
"requirement_type": "dildo_best_float",
"requirement_value": 1,
"reward_amount": 50000,
"sort_order": 113,
},
{
"name": "dildo_float_0005",
"title": "Безупречный Dildo",
"description": "Получи Dildo с float < 0.005",
"icon": "",
"category": "general",
"requirement_type": "dildo_best_float",
"requirement_value": 2,
"reward_amount": 250000,
"sort_order": 114,
},
{
"name": "dildo_from_contract",
"title": "Контракт на Dildo",
"description": "Получи Dildo из контракта обмена",
"icon": "🔄",
"category": "general",
"requirement_type": "dildo_contract_count",
"requirement_value": 1,
"reward_amount": 20000,
"sort_order": 115,
},
{
"name": "dildo_all_rarities",
"title": "Гамма Dildo",
"description": "Собери Dildo каждой редкости",
"icon": "🌈",
"category": "general",
"requirement_type": "dildo_all_rarities",
"requirement_value": 1,
"reward_amount": 100000,
"sort_order": 116,
},
{
"name": "ultra_rare_float",
"title": "Совершенство",
"description": "Получи любой предмет с float 0.000X",
"icon": "💎",
"category": "general",
"requirement_type": "ultra_rare_float",
"requirement_value": 0.001,
"reward_amount": 500000,
"sort_order": 117,
},
{
"name": "dildo_full_set",
"title": "Полная коллекция",
"description": "Собери 25 уникальных Dildo разных узоров",
"icon": "🏆",
"category": "general",
"requirement_type": "dildo_unique_count",
"requirement_value": 25,
"reward_amount": 1000000,
"sort_order": 118,
},
{
"name": "dildo_worth_100m",
"title": "Dildo-магнат",
"description": "Общая стоимость всех Dildo в инвентаре > 100 000 000 ₽",
"icon": "💰",
"category": "general",
"requirement_type": "dildo_total_value",
"requirement_value": 100000000,
"reward_amount": 500000,
"sort_order": 119,
},
# ═══════════════════════════════════════════════════════════════════════════
# ❓ СЕКРЕТНЫЕ (15 новых)
# ═══════════════════════════════════════════════════════════════════════════
{
"name": "secret_midnight",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_midnight_case",
"requirement_value": 1,
"reward_amount": 5000,
"sort_order": 200,
"hidden": True,
},
{
"name": "secret_exact_balance",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_exact_balance",
"requirement_value": 1337,
"reward_amount": 5000,
"sort_order": 201,
"hidden": True,
},
{
"name": "secret_99_fail",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_99_fail",
"requirement_value": 1,
"reward_amount": 10000,
"sort_order": 202,
"hidden": True,
},
{
"name": "secret_negative_balance",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_negative_balance",
"requirement_value": 1,
"reward_amount": 5000,
"sort_order": 203,
"hidden": True,
},
{
"name": "secret_1000_exact",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_exact_cases",
"requirement_value": 1000,
"reward_amount": 10000,
"sort_order": 204,
"hidden": True,
},
{
"name": "secret_empty_inventory",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_empty_inv",
"requirement_value": 1,
"reward_amount": 5000,
"sort_order": 205,
"hidden": True,
},
{
"name": "secret_spent_exact",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_spent_exact",
"requirement_value": 666666,
"reward_amount": 15000,
"sort_order": 206,
"hidden": True,
},
{
"name": "secret_low_prob_win",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_low_prob_win",
"requirement_value": 1,
"reward_amount": 10000,
"sort_order": 207,
"hidden": True,
},
{
"name": "secret_dildo_specific",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_dildo_specific",
"requirement_value": 1,
"reward_amount": 5000,
"sort_order": 208,
"hidden": True,
},
{
"name": "secret_crash_1x",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_crash_1x",
"requirement_value": 1,
"reward_amount": 5000,
"sort_order": 209,
"hidden": True,
},
{
"name": "secret_balance_777",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_balance_777",
"requirement_value": 777,
"reward_amount": 7777,
"sort_order": 210,
"hidden": True,
},
{
"name": "secret_one_item_inv",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_one_item_inv",
"requirement_value": 1,
"reward_amount": 5000,
"sort_order": 211,
"hidden": True,
},
{
"name": "secret_contract_dildo",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_contract_dildo",
"requirement_value": 1,
"reward_amount": 10000,
"sort_order": 212,
"hidden": True,
},
{
"name": "secret_slot_zero",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_slot_zero",
"requirement_value": 1,
"reward_amount": 15000,
"sort_order": 213,
"hidden": True,
},
{
"name": "secret_exact_float",
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": "general",
"requirement_type": "secret_exact_float",
"requirement_value": 1,
"reward_amount": 50000,
"sort_order": 214,
"hidden": True,
},
# ═══════════════════════════════════════════════════════════════════════════
# 🎨 КОЛЛЕКЦИИ (25 новых)
# ═══════════════════════════════════════════════════════════════════════════
{
"name": "coll_first",
"title": "Первая коллекция",
"description": "Получи предмет, входящий в коллекцию",
"icon": "🎨",
"category": "general",
"requirement_type": "collection_count",
"requirement_value": 1,
"reward_amount": 500,
"sort_order": 120,
},
{
"name": "coll_5",
"title": "Начинающий коллекционер",
"description": "Собери предметы из 5 разных коллекций",
"icon": "🎨",
"category": "general",
"requirement_type": "collection_count",
"requirement_value": 5,
"reward_amount": 1000,
"sort_order": 121,
},
{
"name": "coll_10",
"title": "Коллекционер",
"description": "Собери предметы из 10 разных коллекций",
"icon": "🎨",
"category": "general",
"requirement_type": "collection_count",
"requirement_value": 10,
"reward_amount": 2500,
"sort_order": 122,
},
{
"name": "coll_25",
"title": "Знаток коллекций",
"description": "Собери предметы из 25 разных коллекций",
"icon": "🏛️",
"category": "general",
"requirement_type": "collection_count",
"requirement_value": 25,
"reward_amount": 5000,
"sort_order": 123,
},
{
"name": "coll_50",
"title": "Мастер коллекций",
"description": "Собери предметы из 50 разных коллекций",
"icon": "🏛️",
"category": "general",
"requirement_type": "collection_count",
"requirement_value": 50,
"reward_amount": 10000,
"sort_order": 124,
},
{
"name": "coll_75",
"title": "Хранитель коллекций",
"description": "Собери предметы из 75 разных коллекций",
"icon": "🏛️",
"category": "general",
"requirement_type": "collection_count",
"requirement_value": 75,
"reward_amount": 25000,
"sort_order": 125,
},
{
"name": "coll_100",
"title": "Архивариус",
"description": "Собери предметы из 100 разных коллекций",
"icon": "📚",
"category": "general",
"requirement_type": "collection_count",
"requirement_value": 100,
"reward_amount": 50000,
"sort_order": 126,
},
{
"name": "coll_all_base",
"title": "Полная коллекция",
"description": "Собери все стандартные коллекции (без кастомных)",
"icon": "👑",
"category": "general",
"requirement_type": "collection_all_base",
"requirement_value": 1,
"reward_amount": 200000,
"sort_order": 127,
},
{
"name": "coll_all",
"title": "Абсолютная коллекция",
"description": "Собери все коллекции, включая кастомные",
"icon": "👑",
"category": "general",
"requirement_type": "collection_all",
"requirement_value": 1,
"reward_amount": 1000000,
"sort_order": 128,
},
{
"name": "coll_items_10",
"title": "Собиратель",
"description": "Собери 10 уникальных предметов из коллекций",
"icon": "🎨",
"category": "general",
"requirement_type": "collection_items",
"requirement_value": 10,
"reward_amount": 500,
"sort_order": 129,
},
{
"name": "coll_items_50",
"title": "Коллекция растёт",
"description": "Собери 50 уникальных предметов из коллекций",
"icon": "🎨",
"category": "general",
"requirement_type": "collection_items",
"requirement_value": 50,
"reward_amount": 2000,
"sort_order": 130,
},
{
"name": "coll_items_100",
"title": "Галерея",
"description": "Собери 100 уникальных предметов из коллекций",
"icon": "🖼️",
"category": "general",
"requirement_type": "collection_items",
"requirement_value": 100,
"reward_amount": 5000,
"sort_order": 131,
},
{
"name": "coll_items_250",
"title": "Музей",
"description": "Собери 250 уникальных предметов из коллекций",
"icon": "🖼️",
"category": "general",
"requirement_type": "collection_items",
"requirement_value": 250,
"reward_amount": 15000,
"sort_order": 132,
},
{
"name": "coll_items_500",
"title": "Эрмитаж",
"description": "Собери 500 уникальных предметов из коллекций",
"icon": "🏛️",
"category": "general",
"requirement_type": "collection_items",
"requirement_value": 500,
"reward_amount": 50000,
"sort_order": 133,
},
{
"name": "coll_items_1000",
"title": "Лувр",
"description": "Собери 1000 уникальных предметов из коллекций",
"icon": "🏛️",
"category": "general",
"requirement_type": "collection_items",
"requirement_value": 1000,
"reward_amount": 150000,
"sort_order": 134,
},
{
"name": "coll_complete_1",
"title": "Первая полная",
"description": "Собери все скины одной коллекции (полный сет)",
"icon": "🎯",
"category": "general",
"requirement_type": "collection_complete_count",
"requirement_value": 1,
"reward_amount": 5000,
"sort_order": 135,
},
{
"name": "coll_complete_5",
"title": "Набор коллекций",
"description": "Собери полностью 5 разных коллекций",
"icon": "🎯",
"category": "general",
"requirement_type": "collection_complete_count",
"requirement_value": 5,
"reward_amount": 20000,
"sort_order": 136,
},
{
"name": "coll_complete_10",
"title": "Коллекционер-перфекционист",
"description": "Собери полностью 10 разных коллекций",
"icon": "🎯",
"category": "general",
"requirement_type": "collection_complete_count",
"requirement_value": 10,
"reward_amount": 50000,
"sort_order": 137,
},
{
"name": "coll_complete_25",
"title": "Перфекционист",
"description": "Собери полностью 25 разных коллекций",
"icon": "🏆",
"category": "general",
"requirement_type": "collection_complete_count",
"requirement_value": 25,
"reward_amount": 150000,
"sort_order": 138,
},
{
"name": "coll_complete_50",
"title": "Абсолютный перфекционист",
"description": "Собери полностью 50 разных коллекций",
"icon": "🏆",
"category": "general",
"requirement_type": "collection_complete_count",
"requirement_value": 50,
"reward_amount": 500000,
"sort_order": 139,
},
{
"name": "coll_cheap_collector",
"title": "От простого",
"description": "Собери все Consumer Grade скины любой коллекции",
"icon": "",
"category": "general",
"requirement_type": "coll_rarity_complete",
"requirement_value": 1,
"reward_amount": 1000,
"sort_order": 140,
},
{
"name": "coll_mid_collector",
"title": "Середина",
"description": "Собери все Mil-Spec скины любой коллекции",
"icon": "🟦",
"category": "general",
"requirement_type": "coll_rarity_complete",
"requirement_value": 2,
"reward_amount": 3000,
"sort_order": 141,
},
{
"name": "coll_rare_collector",
"title": "Элита",
"description": "Собери все Restricted+ скины любой коллекции",
"icon": "🟣",
"category": "general",
"requirement_type": "coll_rarity_complete",
"requirement_value": 3,
"reward_amount": 10000,
"sort_order": 142,
},
{
"name": "coll_covert_collector",
"title": "Крем",
"description": "Собери все Covert скины любой коллекции",
"icon": "🔴",
"category": "general",
"requirement_type": "coll_rarity_complete",
"requirement_value": 4,
"reward_amount": 30000,
"sort_order": 143,
},
{
"name": "coll_full_case",
"title": "Кейс закрыт",
"description": "Собери все предметы из одного кейса",
"icon": "📦",
"category": "general",
"requirement_type": "coll_case_complete",
"requirement_value": 1,
"reward_amount": 5000,
"sort_order": 144,
},
]
# ─── Функции проверки достижений ──────────────────────────────────────────
def check_collection_achievements(db: Session, user_id: int):
"""Проверяет достижения, связанные с коллекциями"""
from backend import ALL_ITEMS, COLLECTIONS_INDEX, extract_base_name
from database import InventoryItem
results = []
# Все предметы пользователя
user_inv = db.query(InventoryItem.item_id).filter(
InventoryItem.user_id == user_id
).all()
user_ids = {iid for (iid,) in user_inv}
# Собираем все коллекции, в которых есть предметы пользователя
# и уникальные базовые имена предметов из коллекций
user_collections = set()
user_coll_base_names = {} # collection_name → set of base names owned
all_coll_base_names = {} # collection_name → set of all base names in collection
for coll_name, item_ids in COLLECTIONS_INDEX.items():
base_set = set()
for cid in item_ids:
item = ALL_ITEMS[cid] if cid < len(ALL_ITEMS) else None
if item:
base = extract_base_name(item.get("market_hash_name", ""))
if base:
base_set.add(base)
if base_set:
all_coll_base_names[coll_name] = base_set
# Check if user owns any item from this collection
owned_bases = set()
for cid in item_ids:
if cid in user_ids:
item = ALL_ITEMS[cid] if cid < len(ALL_ITEMS) else None
if item:
base = extract_base_name(item.get("market_hash_name", ""))
if base:
owned_bases.add(base)
if owned_bases:
user_collections.add(coll_name)
user_coll_base_names[coll_name] = owned_bases
# collection_count: unique collections user has items from
coll_count = len(user_collections)
for n in ["coll_first", "coll_5", "coll_10", "coll_25", "coll_50", "coll_75", "coll_100"]:
r = check_achievement(db, user_id, n, coll_count)
if r: results.append(r)
# collection_items: unique base items from collections owned
all_owned_bases = set()
for bases in user_coll_base_names.values():
all_owned_bases.update(bases)
total_unique_items = len(all_owned_bases)
for n in ["coll_items_10", "coll_items_50", "coll_items_100", "coll_items_250",
"coll_items_500", "coll_items_1000"]:
r = check_achievement(db, user_id, n, total_unique_items)
if r: results.append(r)
# collection_complete_count: collections where user owns ALL base items
complete_count = 0
for coll_name, all_bases in all_coll_base_names.items():
owned_bases = user_coll_base_names.get(coll_name, set())
if all_bases.issubset(owned_bases):
complete_count += 1
for n in ["coll_complete_1", "coll_complete_5", "coll_complete_10", "coll_complete_25",
"coll_complete_50"]:
r = check_achievement(db, user_id, n, complete_count)
if r: results.append(r)
# coll_all_base: all NON-CUSTOM collections complete
non_custom_collections = {}
for coll_name, item_ids in COLLECTIONS_INDEX.items():
# Check if any item in this collection is custom
is_custom = False
for cid in item_ids:
item = ALL_ITEMS[cid] if cid < len(ALL_ITEMS) else None
if item and item.get("price_source") == "custom":
is_custom = True
break
if not is_custom and coll_name in all_coll_base_names:
non_custom_collections[coll_name] = all_coll_base_names[coll_name]
all_base_complete = True
for coll_name, all_bases in non_custom_collections.items():
owned_bases = user_coll_base_names.get(coll_name, set())
if not all_bases.issubset(owned_bases):
all_base_complete = False
break
if all_base_complete and non_custom_collections:
r = check_achievement(db, user_id, "coll_all_base", 1)
if r: results.append(r)
# coll_all: ALL collections complete (including custom)
all_complete = True
for coll_name, all_bases in all_coll_base_names.items():
owned_bases = user_coll_base_names.get(coll_name, set())
if not all_bases.issubset(owned_bases):
all_complete = False
break
if all_complete and all_coll_base_names:
r = check_achievement(db, user_id, "coll_all", 1)
if r: results.append(r)
# coll_rarity_complete: complete all items of specific rarity tiers in any collection
from backend import RARITY_ORDER
rarity_collectors = [
("coll_cheap_collector", {0}), # Consumer Grade
("coll_mid_collector", {2}), # Mil-Spec / Mil-Spec Grade
("coll_rare_collector", {3, 4, 5, 6, 7, 8}), # Restricted+
("coll_covert_collector", {5}), # Covert
]
for ach_name, target_levels in rarity_collectors:
found = False
for coll_name in all_coll_base_names:
# Collect all items at target rarity levels for this collection
all_target = set()
owned_target = set()
for cid in COLLECTIONS_INDEX.get(coll_name, []):
item = ALL_ITEMS[cid] if cid < len(ALL_ITEMS) else None
if not item:
continue
base = extract_base_name(item.get("market_hash_name", ""))
if not base:
continue
rl = RARITY_ORDER.get(item.get("rarity", ""), -1)
if rl in target_levels:
all_target.add(base)
if cid in user_ids:
owned_target.add(base)
if all_target and all_target.issubset(owned_target):
found = True
break
if found:
r = check_achievement(db, user_id, ach_name, 1)
if r: results.append(r)
# coll_case_complete: complete all items from a case
for coll_name, all_bases in all_coll_base_names.items():
if not coll_name.startswith("Case:"):
continue
owned_bases = user_coll_base_names.get(coll_name, set())
if all_bases.issubset(owned_bases):
r = check_achievement(db, user_id, "coll_full_case", 1)
if r: results.append(r)
break
# Also check non-case named collections
for coll_name, all_bases in all_coll_base_names.items():
if coll_name.startswith("Case:"):
continue
owned_bases = user_coll_base_names.get(coll_name, set())
if all_bases.issubset(owned_bases):
# This also counts for coll_full_case if user wants case completion
pass # Already counted in collection_complete_count above
return results
def seed_achievements(db: Session):
existing = db.query(Achievement).count()
if existing > 0:
# Добавляем только новые (отсутствующие в БД)
existing_names = {a.name for a in db.query(Achievement.name).all()}
added = 0
for defn in ACHIEVEMENT_DEFS:
if defn["name"] not in existing_names:
ach = Achievement(**defn)
db.add(ach)
added += 1
if added:
db.commit()
print(f"[Achievements] Добавлено {added} новых достижений")
return
for defn in ACHIEVEMENT_DEFS:
ach = Achievement(**defn)
db.add(ach)
db.commit()
def get_or_create_user_achievement(db: Session, user_id: int, achievement_id: int) -> UserAchievement:
ua = db.query(UserAchievement).filter(
UserAchievement.user_id == user_id,
UserAchievement.achievement_id == achievement_id
).first()
if not ua:
ua = UserAchievement(user_id=user_id, achievement_id=achievement_id)
db.add(ua)
db.commit()
db.refresh(ua)
return ua
def check_achievement(db: Session, user_id: int, achievement_name: str, current_value: float) -> Optional[dict]:
"""Проверяет и разблокирует достижение. Возвращает dict с инфой если разблокировано."""
ach = db.query(Achievement).filter(Achievement.name == achievement_name).first()
if not ach:
return None
ua = get_or_create_user_achievement(db, user_id, ach.id)
if ua.unlocked_at:
return None
progress_val = min(int(current_value), ach.requirement_value) if isinstance(ach.requirement_value, int) else min(current_value, ach.requirement_value)
ua.progress = int(progress_val) if ach.requirement_type not in ("dildo_best_float", "ultra_rare_float", "secret_exact_float") else int(progress_val * 10000)
if current_value >= ach.requirement_value:
ua.unlocked_at = datetime.utcnow()
from database import User
user = db.query(User).filter(User.id == user_id).first()
if user:
user.balance += ach.reward_amount
db.commit()
activity = ActivityFeed(
user_id=user_id,
username=user.username if user else "unknown",
activity_type="achievement",
message=f"🏆 Получено достижение: {ach.title}",
data_json=json.dumps({
"achievement_name": ach.name,
"achievement_title": ach.title,
"achievement_icon": ach.icon,
})
)
db.add(activity)
db.commit()
return {
"name": ach.name,
"title": ach.title,
"icon": ach.icon,
"reward": ach.reward_amount
}
db.commit()
return None
# ─── Существующие проверки (расширенные) ───────────────────────────────────
def check_open_cases_achievements(db: Session, user_id: int):
from database import CaseOpening
count = db.query(CaseOpening).filter(CaseOpening.user_id == user_id).count()
names = ["first_case", "case_collector_10", "case_collector_100", "case_collector_1000",
"cases_2500", "cases_5000", "cases_10000", "cases_25000"]
results = []
for n in names:
r = check_achievement(db, user_id, n, count)
if r: results.append(r)
return results
def check_contracts_achievements(db: Session, user_id: int):
from database import Contract
count = db.query(Contract).filter(Contract.user_id == user_id).count()
names = ["first_contract", "contract_master", "contracts_10", "contracts_25", "contracts_100", "contracts_250"]
results = []
for n in names:
r = check_achievement(db, user_id, n, count)
if r: results.append(r)
return results
def check_upgrade_achievements(db: Session, user_id: int):
from database import Upgrade
attempts = db.query(Upgrade).filter(Upgrade.user_id == user_id).count()
wins_count = db.query(Upgrade).filter(Upgrade.user_id == user_id, Upgrade.success == True).count()
results = []
# upgrade_attempts based
for n in ["first_upgrade", "upgrade_5", "upgrade_25", "upgrade_50", "upgrade_100",
"upgrade_250", "upgrade_500", "upgrade_1000", "upgrade_2000"]:
r = check_achievement(db, user_id, n, attempts)
if r: results.append(r)
# upgrade_wins based
for n in ["upgrade_winner", "upgrade_wins_5", "upgrade_wins_25", "upgrade_wins_50",
"upgrade_wins_100", "upgrade_wins_250"]:
r = check_achievement(db, user_id, n, wins_count)
if r: results.append(r)
# Upgrade total spent value (look up price from ALL_ITEMS for failed upgrades)
from backend import ALL_ITEMS
failed_upgrades = db.query(Upgrade.input_item_id).filter(
Upgrade.user_id == user_id, Upgrade.success == False
).all()
total_spent_val = 0
for (iid,) in failed_upgrades:
if iid < len(ALL_ITEMS):
total_spent_val += ALL_ITEMS[iid].get("price_rub", 0)
for n in ["upgrade_spent_100k", "upgrade_spent_1m", "upgrade_spent_10m", "upgrade_spent_100m"]:
r = check_achievement(db, user_id, n, total_spent_val)
if r: results.append(r)
return results
def check_upgrade_risk_achievements(db: Session, user_id: int, item_price: float):
"""Вызывается при каждой попытке апгрейда — проверяет рискованные ставки"""
results = []
for n in ["upgrade_high_risk_50k", "upgrade_high_risk_500k", "upgrade_high_risk_10m"]:
r = check_achievement(db, user_id, n, item_price)
if r: results.append(r)
return results
def check_upgrade_result_achievements(db: Session, user_id: int, success: bool, probability: float, item_price: float):
"""Вызывается после завершения апгрейда — проверяет серии, чудеса и т.д."""
from database import Upgrade
results = []
# Consecutive wins/losses
all_upgrades = db.query(Upgrade.success).filter(
Upgrade.user_id == user_id
).order_by(Upgrade.id.desc()).all()
streak = 0
streak_type = None # True = wins, False = losses
for (s,) in all_upgrades:
if streak_type is None:
streak_type = s
streak = 1
elif s == streak_type:
streak += 1
else:
break
if streak_type == True:
for n in ["upgrade_consecutive_wins_3", "upgrade_consecutive_wins_5", "upgrade_consecutive_wins_10"]:
r = check_achievement(db, user_id, n, streak)
if r: results.append(r)
elif streak_type == False:
for n in ["upgrade_consecutive_losses_5", "upgrade_consecutive_losses_10"]:
r = check_achievement(db, user_id, n, streak)
if r: results.append(r)
# Low value win
if success and item_price <= 1000:
r = check_achievement(db, user_id, "upgrade_from_nothing", 1000)
if r: results.append(r)
# Low probability win
if success and probability < 10:
low_prob_wins = db.query(Upgrade).filter(
Upgrade.user_id == user_id, Upgrade.success == True, Upgrade.probability < 10
).count()
for n in ["upgrade_miracle", "upgrade_miracle_5"]:
r = check_achievement(db, user_id, n, low_prob_wins)
if r: results.append(r)
# Secret: 99% fail
if not success and probability >= 99:
r = check_achievement(db, user_id, "secret_99_fail", 1)
if r: results.append(r)
# Secret: low prob win
if success and probability < 10:
r = check_achievement(db, user_id, "secret_low_prob_win", 1)
if r: results.append(r)
return results
def check_rare_item_achievement(db: Session, user_id: int, rarity: str):
results = []
if rarity in ("Covert", "Extraordinary", "Rare Special Item"):
r = check_achievement(db, user_id, "rare_finder", 1)
if r: results.append(r)
return results
def check_knife_achievement(db: Session, user_id: int, item_data: dict):
from backend import is_knife, is_gloves
results = []
if is_knife(item_data) or is_gloves(item_data):
r = check_achievement(db, user_id, "knife_collector", 1)
if r: results.append(r)
return results
def check_spent_achievements(db: Session, user_id: int, total_spent: float):
names = ["big_spender", "whale", "spent_500k", "spent_1m", "spent_10m",
"spent_100m", "spent_500m", "spent_1b"]
results = []
for n in names:
r = check_achievement(db, user_id, n, int(total_spent))
if r: results.append(r)
# Secret: exact spent 666666
if int(total_spent) == 666666:
r = check_achievement(db, user_id, "secret_spent_exact", 1)
if r: results.append(r)
return results
def check_balance_achievements(db: Session, user_id: int):
from database import User
user = db.query(User).filter(User.id == user_id).first()
if not user:
return []
results = []
# rich_100k legacy — DB has requirement_value=1, pass flag when condition met
if user.balance >= 100000:
r = check_achievement(db, user_id, "rich_100k", 1)
if r: results.append(r)
for n in ["balance_1m", "balance_10m", "balance_100m", "balance_1b"]:
r = check_achievement(db, user_id, n, user.balance)
if r: results.append(r)
# Secret: exact balance 1337
if user.balance == 1337:
r = check_achievement(db, user_id, "secret_exact_balance", 1)
if r: results.append(r)
# Secret: balance % 1000 == 777
if user.balance > 0 and user.balance % 1000 == 777:
r = check_achievement(db, user_id, "secret_balance_777", 777)
if r: results.append(r)
# Secret: negative balance
if user.balance < 0:
r = check_achievement(db, user_id, "secret_negative_balance", 1)
if r: results.append(r)
return results
def check_slot_match_achievements(db: Session, user_id: int):
results = []
r = check_achievement(db, user_id, "slot_winner", 1)
if r: results.append(r)
return results
def check_slot_plays_achievements(db: Session, user_id: int):
from database import CaseOpening
slot_count = db.query(CaseOpening).filter(
CaseOpening.user_id == user_id,
CaseOpening.case_name.ilike("%slot%")
).count()
names = ["slots_10", "slots_25", "slots_100", "slots_500"]
results = []
for n in names:
r = check_achievement(db, user_id, n, slot_count)
if r: results.append(r)
return results
def check_slot_match_count_achievements(db: Session, user_id: int):
"""Проверяет количество совпадений в слотах (считаем по бонусным предметам)"""
from database import InventoryItem
matches = db.query(InventoryItem).filter(
InventoryItem.user_id == user_id,
InventoryItem.obtained_from.ilike("slot_bonus:%")
).count()
results = []
for n in ["slots_jackpot_5", "slots_jackpot_25"]:
r = check_achievement(db, user_id, n, matches)
if r: results.append(r)
return results
def check_inventory_achievements(db: Session, user_id: int):
from database import InventoryItem
count = db.query(InventoryItem).filter(InventoryItem.user_id == user_id).count()
names = ["inventory_50", "inventory_100", "inventory_250", "inventory_500",
"inventory_1000", "inventory_2500"]
results = []
for n in names:
r = check_achievement(db, user_id, n, count)
if r: results.append(r)
# Secret: empty inventory
if count == 0:
r = check_achievement(db, user_id, "secret_empty_inv", 1)
if r: results.append(r)
# Secret: exactly 1 item
if count == 1:
r = check_achievement(db, user_id, "secret_one_item_inv", 1)
if r: results.append(r)
return results
def check_dildo_achievements(db: Session, user_id: int):
"""Проверяет все Dildo-связанные достижения"""
from database import InventoryItem
from backend import ALL_ITEMS
results = []
items = db.query(InventoryItem).filter(InventoryItem.user_id == user_id).all()
dildo_items = []
for inv_item in items:
if inv_item.item_id < len(ALL_ITEMS):
item_data = ALL_ITEMS[inv_item.item_id]
if item_data.get("weapon") == "Dildo":
dildo_items.append((inv_item, item_data))
dildo_count = len(dildo_items)
# Dildo count achievements
for n in ["dildo_owned", "dildo_5", "dildo_10", "dildo_25", "dildo_50", "dildo_100"]:
r = check_achievement(db, user_id, n, dildo_count)
if r: results.append(r)
# Best item price in inventory
best_price = 0
for inv_item in items:
if inv_item.item_id < len(ALL_ITEMS):
p = ALL_ITEMS[inv_item.item_id].get("price_rub", 0)
if p > best_price:
best_price = p
for n in ["item_worth_1m", "item_worth_10m", "item_worth_100m", "item_worth_1b"]:
r = check_achievement(db, user_id, n, best_price)
if r: results.append(r)
# Total inventory value
total_val = 0
for inv_item in items:
if inv_item.item_id < len(ALL_ITEMS):
total_val += ALL_ITEMS[inv_item.item_id].get("price_rub", 0)
for n in ["inventory_value_10m", "inventory_value_100m", "inventory_value_1b"]:
r = check_achievement(db, user_id, n, total_val)
if r: results.append(r)
# Dildo float achievements
best_float = 1.0
for inv_item, item_data in dildo_items:
f = inv_item.float_value or 1.0
if f < best_float:
best_float = f
if best_float < 0.01:
r = check_achievement(db, user_id, "dildo_float_001", 1)
if r: results.append(r)
if best_float < 0.005:
r = check_achievement(db, user_id, "dildo_float_0005", 2)
if r: results.append(r)
# Ultra rare float (any item)
ultra_rare = 1.0
for inv_item in items:
f = inv_item.float_value or 1.0
if f < ultra_rare:
ultra_rare = f
if ultra_rare < 0.001:
r = check_achievement(db, user_id, "ultra_rare_float", 0.001)
if r: results.append(r)
# Secret: exact float 0.001 (or close)
for inv_item in items:
f = inv_item.float_value or 0
if abs(f - 0.001) < 0.0001:
r = check_achievement(db, user_id, "secret_exact_float", 1)
if r: results.append(r)
break
# Dildo all rarities
dildo_rarities = set()
for inv_item, item_data in dildo_items:
dildo_rarities.add(item_data.get("rarity"))
if len(dildo_rarities) >= 5: # Consumer, Industrial, Mil-Spec, Restricted, Classified, Covert
r = check_achievement(db, user_id, "dildo_all_rarities", 1)
if r: results.append(r)
# Dildo unique count (by pattern/name)
unique_patterns = set()
for inv_item, item_data in dildo_items:
name = item_data.get("market_hash_name", "")
unique_patterns.add(name)
r = check_achievement(db, user_id, "dildo_full_set", len(unique_patterns))
if r: results.append(r)
# Dildo total value
dildo_total = 0
for inv_item, item_data in dildo_items:
dildo_total += item_data.get("price_rub", 0)
r = check_achievement(db, user_id, "dildo_worth_100m", dildo_total)
if r: results.append(r)
# Secret: specific dildo name
for inv_item, item_data in dildo_items:
name = item_data.get("market_hash_name", "")
if "Big Black" in name:
r = check_achievement(db, user_id, "secret_dildo_specific", 1)
if r: results.append(r)
break
return results
def check_dildo_contract_achievement(db: Session, user_id: int):
"""Проверяет получение Dildo из контракта"""
from database import Contract, InventoryItem
results = []
# Check if any contract output is a Dildo
from backend import ALL_ITEMS
contracts = db.query(Contract).filter(Contract.user_id == user_id).all()
for c in contracts:
if c.output_item_id < len(ALL_ITEMS):
item_data = ALL_ITEMS[c.output_item_id]
if item_data.get("weapon") == "Dildo":
r = check_achievement(db, user_id, "dildo_from_contract", 1)
if r: results.append(r)
break
# Secret: contract gave dildo
for c in contracts:
if c.output_item_id < len(ALL_ITEMS):
item_data = ALL_ITEMS[c.output_item_id]
if item_data.get("weapon") == "Dildo":
r = check_achievement(db, user_id, "secret_contract_dildo", 1)
if r: results.append(r)
break
return results
def check_crash_achievements(db: Session, user_id: int, crash_count: int):
from database import ActivityFeed as CrActivity
results = []
for n in ["crash_first", "crash_pro", "crash_10", "crash_25", "crash_100",
"crash_500", "crash_1000", "crash_5000"]:
r = check_achievement(db, user_id, n, crash_count)
if r: results.append(r)
return results
def check_crash_cashout_achievements(db: Session, user_id: int, multiplier: float):
results = []
for n in ["crash_5x", "crash_cashout_10x", "crash_cashout_25x", "crash_cashout_50x", "crash_cashout_100x"]:
req_val = {"crash_5x": 5, "crash_cashout_10x": 10, "crash_cashout_25x": 25,
"crash_cashout_50x": 50, "crash_cashout_100x": 100}[n]
if multiplier >= req_val:
r = check_achievement(db, user_id, n, multiplier)
if r: results.append(r)
# Secret: crash 1x
if abs(multiplier - 1.0) < 0.01:
r = check_achievement(db, user_id, "secret_crash_1x", 1)
if r: results.append(r)
return results
def check_items_unboxed_achievements(db: Session, user_id: int):
from database import CaseOpening
count = db.query(CaseOpening).filter(CaseOpening.user_id == user_id).count()
results = []
for n in ["items_unboxed_100", "items_unboxed_1000", "items_unboxed_10000"]:
r = check_achievement(db, user_id, n, count)
if r: results.append(r)
return results
def check_total_profit_achievements(db: Session, user_id: int, profit: float):
results = []
for n in ["total_profit_100k", "total_profit_1m", "total_profit_10m", "total_profit_100m"]:
r = check_achievement(db, user_id, n, profit)
if r: results.append(r)
return results
def check_achievements_count_achievements(db: Session, user_id: int):
count = db.query(UserAchievement).filter(
UserAchievement.user_id == user_id,
UserAchievement.unlocked_at.isnot(None)
).count()
results = []
for n in ["achievements_10", "achievements_25", "achievements_50", "achievements_100"]:
r = check_achievement(db, user_id, n, count)
if r: results.append(r)
return results
def check_rare_item_count_achievements(db: Session, user_id: int):
from database import InventoryItem
from backend import ALL_ITEMS
items = db.query(InventoryItem).filter(InventoryItem.user_id == user_id).all()
rare_count = 0
for inv in items:
if inv.item_id < len(ALL_ITEMS):
rar = ALL_ITEMS[inv.item_id].get("rarity", "")
if rar in ("Covert", "Extraordinary", "Rare Special Item"):
rare_count += 1
results = []
for n in ["rare_item_10", "rare_item_50"]:
r = check_achievement(db, user_id, n, rare_count)
if r: results.append(r)
return results
def check_knife_count_achievements(db: Session, user_id: int):
from database import InventoryItem
from backend import ALL_ITEMS, is_knife, is_gloves
items = db.query(InventoryItem).filter(InventoryItem.user_id == user_id).all()
knife_count = 0
for inv in items:
if inv.item_id < len(ALL_ITEMS):
item_data = ALL_ITEMS[inv.item_id]
if is_knife(item_data) or is_gloves(item_data):
knife_count += 1
results = []
for n in ["knife_5"]:
r = check_achievement(db, user_id, n, knife_count)
if r: results.append(r)
return results
def check_midnight_case_achievement(db: Session, user_id: int) -> list:
"""Проверяет секретное достижение 'открытие кейса в полночь'"""
from database import CaseOpening
from sqlalchemy import func
midnight_openings = db.query(CaseOpening).filter(
CaseOpening.user_id == user_id,
func.strftime("%H", CaseOpening.opened_at).between("00", "04")
).count()
if midnight_openings > 0:
r = check_achievement(db, user_id, "secret_midnight", 1)
return [r] if r else []
return []
def check_exact_cases_achievement(db: Session, user_id: int, case_count: int) -> list:
"""Проверяет секретное достижение 'ровно 1000 кейсов'"""
result = check_achievement(db, user_id, "secret_1000_exact", 1 if case_count == 1000 else 0)
return [result] if result else []
def check_all_secret_achievements(db: Session, user_id: int):
"""Проверяет все секретные достижения (вызывается периодически)"""
results = []
# These are checked in their respective functions above:
# secret_midnight - check_midnight_case_achievement
# secret_exact_balance - check_balance_achievements
# secret_99_fail - check_upgrade_result_achievements
# secret_negative_balance - check_balance_achievements
# secret_1000_exact - check_exact_cases_achievement
# secret_empty_inv - check_inventory_achievements
# secret_spent_exact - check_spent_achievements
# secret_low_prob_win - check_upgrade_result_achievements
# secret_dildo_specific - check_dildo_achievements
# secret_crash_1x - check_crash_cashout_achievements
# secret_balance_777 - check_balance_achievements
# secret_one_item_inv - check_inventory_achievements
# secret_contract_dildo - check_dildo_contract_achievement
# secret_slot_zero - need to implement
# secret_exact_float - check_dildo_achievements
return results
# ─── Получение достижений пользователя ─────────────────────────────────────
def get_user_achievements(db: Session, user_id: int) -> list:
"""Возвращает все достижения пользователя с прогрессом.
Секретные (hidden) достижения показываются как ??? пока не открыты."""
all_ach = db.query(Achievement).order_by(Achievement.sort_order).all()
result = []
for ach in all_ach:
ua = db.query(UserAchievement).filter(
UserAchievement.user_id == user_id,
UserAchievement.achievement_id == ach.id
).first()
unlocked = ua.unlocked_at is not None if ua else False
# Секретные: скрываем название/описание пока не открыто
if ach.hidden and not unlocked:
result.append({
"id": ach.id,
"name": ach.name,
"title": "???",
"description": "Хммм... похоже что я не знаю как его открыть",
"icon": "",
"category": ach.category,
"requirement_type": ach.requirement_type,
"requirement_value": ach.requirement_value,
"reward_amount": ach.reward_amount,
"progress": 0,
"unlocked": False,
"unlocked_at": None,
"hidden": True,
})
else:
result.append({
"id": ach.id,
"name": ach.name,
"title": ach.title,
"description": ach.description,
"icon": ach.icon,
"category": ach.category,
"requirement_type": ach.requirement_type,
"requirement_value": ach.requirement_value,
"reward_amount": ach.reward_amount,
"progress": ua.progress if ua else 0,
"unlocked": unlocked,
"unlocked_at": ua.unlocked_at.isoformat() if ua and ua.unlocked_at else None,
"hidden": ach.hidden if hasattr(ach, 'hidden') else False,
})
return result