diff --git a/achievements.py b/achievements.py index e58bc55..76fc13b 100644 --- a/achievements.py +++ b/achievements.py @@ -3,10 +3,12 @@ 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": "Первый шаг", @@ -156,8 +158,8 @@ ACHIEVEMENT_DEFS = [ "description": "Накопи 100000 ₽ на балансе", "icon": "🤑", "category": "general", - "requirement_type": "balance_100k", - "requirement_value": 1, + "requirement_type": "balance_milestone", + "requirement_value": 100000, "reward_amount": 5000, "sort_order": 14, }, @@ -200,8 +202,8 @@ ACHIEVEMENT_DEFS = [ "description": "Выведи деньги на множителе x5 или выше в Crash", "icon": "🚀", "category": "crash", - "requirement_type": "crash_cashout_5x", - "requirement_value": 1, + "requirement_type": "crash_cashout_milestone", + "requirement_value": 5, "reward_amount": 1500, "sort_order": 18, }, @@ -216,15 +218,1763 @@ ACHIEVEMENT_DEFS = [ "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) @@ -244,28 +1994,27 @@ def get_or_create_user_achievement(db: Session, user_id: int, achievement_id: in return ua -def check_achievement(db: Session, user_id: int, achievement_name: str, current_value: int) -> Optional[dict]: +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 - - ua.progress = min(current_value, ach.requirement_value) - + + 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", @@ -279,52 +2028,139 @@ def check_achievement(db: Session, user_id: int, achievement_name: str, current_ ) 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 ach_name in ["first_case", "case_collector_10", "case_collector_100", "case_collector_1000"]: - r = check_achievement(db, user_id, ach_name, count) - if r: - results.append(r) + 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 ach_name in ["first_contract", "contract_master"]: - r = check_achievement(db, user_id, ach_name, count) - if r: - results.append(r) + 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 = db.query(Upgrade).filter(Upgrade.user_id == user_id, Upgrade.success == True).count() - + wins_count = db.query(Upgrade).filter(Upgrade.user_id == user_id, Upgrade.success == True).count() + results = [] - r1 = check_achievement(db, user_id, "first_upgrade", attempts) - if r1: results.append(r1) - r2 = check_achievement(db, user_id, "upgrade_winner", wins) - if r2: results.append(r2) + # 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 @@ -346,24 +2182,54 @@ def check_knife_achievement(db: Session, user_id: int, item_data: dict): 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 = [] - r1 = check_achievement(db, user_id, "big_spender", int(total_spent)) - if r1: results.append(r1) - r2 = check_achievement(db, user_id, "whale", int(total_spent)) - if r2: results.append(r2) + 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_achievement(db: Session, user_id: int): +def check_balance_achievements(db: Session, user_id: int): from database import User user = db.query(User).filter(User.id == user_id).first() - if user and user.balance >= 100000: + 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: return [r] - return [] + 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_achievement(db: Session, user_id: int): +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) @@ -376,23 +2242,323 @@ def check_slot_plays_achievements(db: Session, user_id: int): CaseOpening.user_id == user_id, CaseOpening.case_name.ilike("%slot%") ).count() + names = ["slots_10", "slots_25", "slots_100", "slots_500"] results = [] - r = check_achievement(db, user_id, "slots_10", slot_count) - if r: results.append(r) + for n in names: + r = check_achievement(db, user_id, n, slot_count) + if r: results.append(r) return results -def check_inventory_achievement(db: Session, user_id: int): +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 = [] - r = check_achievement(db, user_id, "inventory_50", count) - if r: results.append(r) + 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: @@ -400,18 +2566,39 @@ def get_user_achievements(db: Session, user_id: int) -> list: UserAchievement.user_id == user_id, UserAchievement.achievement_id == ach.id ).first() - 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": ua.unlocked_at is not None if ua else False, - "unlocked_at": ua.unlocked_at.isoformat() if ua and ua.unlocked_at else None, - }) + 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 diff --git a/admin.py b/admin.py index a9972f8..9c108be 100644 --- a/admin.py +++ b/admin.py @@ -101,8 +101,7 @@ async def admin_dashboard( recent_users = db.query(User).order_by(desc(User.created_at)).limit(10).all() - return templates.TemplateResponse("admin/dashboard.html", { - "request": request, + return templates.TemplateResponse(request, "admin/dashboard.html", { "user": admin, "total_users": total_users, "total_items": total_items, @@ -151,8 +150,7 @@ async def admin_users( "rpu_level": round(calculate_rpu_level(rpu)) }) - return templates.TemplateResponse("admin/users.html", { - "request": request, + return templates.TemplateResponse(request, "admin/users.html", { "user": admin, "users": users_with_rpu, "search": search @@ -177,8 +175,7 @@ async def admin_user_detail( InventoryItem.user_id == user_id ).order_by(desc(InventoryItem.obtained_at)).limit(50).all() - return templates.TemplateResponse("admin/user_detail.html", { - "request": request, + return templates.TemplateResponse(request, "admin/user_detail.html", { "user": admin, "target_user": target_user, "total_items": total_items, @@ -210,8 +207,7 @@ async def admin_cases( "image_url": item.get("image_url", "") }) - return templates.TemplateResponse("admin/cases.html", { - "request": request, + return templates.TemplateResponse(request, "admin/cases.html", { "user": admin, "cases": cases, "mainpage": mainpage, @@ -291,6 +287,9 @@ async def admin_give_item( db.add(inventory_item) db.commit() + from achievements import check_collection_achievements + check_collection_achievements(db, user_id) + return JSONResponse({ "success": True, "message": f"Предмет '{item_data.get('market_hash_name')}' выдан пользователю {target_user.username}" diff --git a/backend.py b/backend.py index c7da223..b59f592 100644 --- a/backend.py +++ b/backend.py @@ -105,27 +105,36 @@ def load_custom_items() -> List[dict]: # ─── Генерация sub-вариаций цен и float ─────────────────────────────────── def generate_item_variants(num_variants: int = 6): - """Для каждого предмета генерирует sub-вариации с разными ценами и float""" + """Для каждого предмета генерирует sub-вариации с разными ценами и float. + + Оригиналы остаются на своих местах (индексы 0..N-1 не меняются), + sub-вариации добавляются в конец списка (индексы N..N+N*5-1), + чтобы не ломать ссылки по индексам (cases.json, база данных). + """ global ALL_ITEMS - new_all = [] - for item in ALL_ITEMS: + n_originals = len(ALL_ITEMS) + sub_variants = [] + + for idx, item in enumerate(ALL_ITEMS): base_price = item.get("price_rub", 0) wr = item.get("wear_range", [0, 1]) mid = item.get("mid_float_used", 0.5) wr_min, wr_max = wr f_range = max(wr_max - wr_min, 0.001) - for v in range(num_variants): + # v=0 — сам оригинал (оставляем как есть) + # v=1..num_variants-1 — sub-вариации (добавляем в конец) + for v in range(1, num_variants): variant = dict(item) + variant["is_subvariant"] = True t = v / (num_variants - 1) if num_variants > 1 else 0 + # price factor: 0.55 to 1.40 across variants factor = 0.55 + t * 0.85 - # add small random jitter factor += random.uniform(-0.05, 0.05) factor = max(0.30, min(1.50, factor)) p = int(base_price * factor) - # scramble last digits if p >= 1_000_000: prefix = p // 1000 suffix = random.randint(100, 999) @@ -148,11 +157,11 @@ def generate_item_variants(num_variants: int = 6): new_mid = max(wr_min + 0.001, min(wr_max - 0.001, new_mid)) variant["mid_float_used"] = round(new_mid, 4) - new_all.append(variant) + sub_variants.append(variant) - count_before = len(ALL_ITEMS) - ALL_ITEMS = new_all - print(f"Сгенерировано {len(ALL_ITEMS)} sub-вариаций из {count_before} предметов (x{num_variants})") + ALL_ITEMS = ALL_ITEMS + sub_variants + print(f"Сгенерировано {len(sub_variants)} sub-вариаций для {n_originals} предметов (x{num_variants})") + print(f"Оригиналы: индексы 0..{n_originals - 1}, sub-вариации: {n_originals}..{len(ALL_ITEMS) - 1}") # ─── Загрузка основных данных ───────────────────────────────────────────── def load_base_items() -> List[dict]: diff --git a/database.py b/database.py index a31e50c..8c2db02 100644 --- a/database.py +++ b/database.py @@ -162,6 +162,7 @@ class Achievement(Base): requirement_value = Column(Integer, nullable=False) # how many reward_amount = Column(Float, default=0.0) # bonus rubles sort_order = Column(Integer, default=0) + hidden = Column(Boolean, default=False) # секретные достижения user_achievements = relationship("UserAchievement", back_populates="achievement") @@ -195,6 +196,19 @@ class ActivityFeed(Base): Base.metadata.create_all(bind=engine) +# Миграция: добавляем колонку hidden если её нет +try: + from sqlalchemy import inspect + inspector = inspect(engine) + cols = [c["name"] for c in inspector.get_columns("achievements")] + if "hidden" not in cols: + from sqlalchemy import text + with engine.connect() as conn: + conn.execute(text("ALTER TABLE achievements ADD COLUMN hidden BOOLEAN DEFAULT 0")) + conn.commit() +except Exception as e: + print(f"[DB] Миграция hidden: {e}") + def get_db(): db = SessionLocal() try: diff --git a/frontend.py b/frontend.py index b15b707..9dc1e06 100644 --- a/frontend.py +++ b/frontend.py @@ -17,10 +17,23 @@ from pathlib import Path from database import get_db, SessionLocal, User, InventoryItem, CaseOpening, Contract, Upgrade, UpgradeRPU from websocket_manager import manager, crash_game from achievements import ( - seed_achievements, check_achievement, check_open_cases_achievements, check_contracts_achievements, - check_upgrade_achievements, check_rare_item_achievement, check_knife_achievement, - check_spent_achievements, check_balance_achievement, check_slot_match_achievement, - check_slot_plays_achievements, check_inventory_achievement, get_user_achievements + seed_achievements, check_achievement, + check_open_cases_achievements, check_contracts_achievements, + check_upgrade_achievements, check_upgrade_risk_achievements, check_upgrade_result_achievements, + check_rare_item_achievement, check_knife_achievement, + check_spent_achievements, + check_balance_achievements, + check_slot_match_achievements, check_slot_plays_achievements, + check_slot_match_count_achievements, + check_inventory_achievements, + check_dildo_achievements, check_dildo_contract_achievement, + check_crash_achievements, check_crash_cashout_achievements, + check_items_unboxed_achievements, check_total_profit_achievements, + check_achievements_count_achievements, + check_rare_item_count_achievements, check_knife_count_achievements, + check_midnight_case_achievement, check_exact_cases_achievement, + check_collection_achievements, + get_user_achievements ) from auth import ( create_user, authenticate_user, create_access_token, @@ -307,8 +320,7 @@ async def index(request: Request, user: Optional[User] = Depends(get_current_use if user: return RedirectResponse(url="/cases") - return templates.TemplateResponse("index.html", { - "request": request, + return templates.TemplateResponse(request, "index.html", { "user": None, "active_page": "home" }) @@ -319,8 +331,7 @@ async def login_page(request: Request, user: Optional[User] = Depends(get_curren if user: return RedirectResponse(url="/profile") - return templates.TemplateResponse("login.html", { - "request": request, + return templates.TemplateResponse(request, "login.html", { "user": None, "active_page": "login" }) @@ -331,8 +342,7 @@ async def register_page(request: Request, user: Optional[User] = Depends(get_cur if user: return RedirectResponse(url="/profile") - return templates.TemplateResponse("register.html", { - "request": request, + return templates.TemplateResponse(request, "register.html", { "user": None, "active_page": "register" }) @@ -399,8 +409,7 @@ async def profile_page( if item.type: types.add(item.type) - return templates.TemplateResponse("profile.html", { - "request": request, + return templates.TemplateResponse(request, "profile.html", { "user": user, "total_items": total_items, "cases_opened": cases_opened, @@ -490,8 +499,7 @@ async def public_profile_page( reverse=True )[:12] - return templates.TemplateResponse("profile.html", { - "request": request, + return templates.TemplateResponse(request, "profile.html", { "user": current_user, "profile_user": profile_user, "is_public": True, @@ -586,8 +594,7 @@ async def cases_page( #print(f"Всего секций для отображения: {len(sections)}") - return templates.TemplateResponse("cases.html", { - "request": request, + return templates.TemplateResponse(request, "cases.html", { "user": user, "sections": sections, "active_page": "cases" @@ -644,8 +651,7 @@ async def contracts_page( has_contract = any(c >= 10 for c in group_counts.values()) has_knife_contract = len(knife_items) >= 10 - return templates.TemplateResponse("contracts.html", { - "request": request, + return templates.TemplateResponse(request, "contracts.html", { "user": user, "inventory_items": all_items + knife_items, "has_contract": has_contract or has_knife_contract, @@ -887,8 +893,9 @@ async def api_open_case( # Проверка достижений unlocked_ach = [] + open_count = db.query(CaseOpening).filter(CaseOpening.user_id == user.id).count() unlocked_ach += check_open_cases_achievements(db, user.id) - total_spent = db.query(CaseOpening).filter(CaseOpening.user_id == user.id).count() * price_per_case + total_spent = open_count * price_per_case unlocked_ach += check_spent_achievements(db, user.id, total_spent) # Проверка на редкий предмет @@ -898,7 +905,18 @@ async def api_open_case( if item_data: unlocked_ach += check_knife_achievement(db, user.id, item_data) - unlocked_ach += check_inventory_achievement(db, user.id) + unlocked_ach += check_inventory_achievements(db, user.id) + unlocked_ach += check_dildo_achievements(db, user.id) + unlocked_ach += check_items_unboxed_achievements(db, user.id) + unlocked_ach += check_rare_item_count_achievements(db, user.id) + unlocked_ach += check_knife_count_achievements(db, user.id) + unlocked_ach += check_balance_achievements(db, user.id) + unlocked_ach += check_achievements_count_achievements(db, user.id) + unlocked_ach += check_midnight_case_achievement(db, user.id) + unlocked_ach += check_exact_cases_achievement(db, user.id, open_count) + unlocked_ach += check_collection_achievements(db, user.id) + profit = user.balance + total_spent + unlocked_ach += check_total_profit_achievements(db, user.id, profit) # Запись активности — каждый предмет отдельно for r in results: @@ -1067,7 +1085,12 @@ async def api_submit_contract( db.commit() unlocked_ach = check_contracts_achievements(db, user.id) - unlocked_ach += check_inventory_achievement(db, user.id) + unlocked_ach += check_inventory_achievements(db, user.id) + unlocked_ach += check_balance_achievements(db, user.id) + unlocked_ach += check_dildo_achievements(db, user.id) + unlocked_ach += check_dildo_contract_achievement(db, user.id) + unlocked_ach += check_collection_achievements(db, user.id) + unlocked_ach += check_achievements_count_achievements(db, user.id) act = record_activity_own_session(user.id, user.username, "contract", f"🔄 выполнил(а) контракт и получил(а) {result.received_item.market_hash_name}", @@ -1148,6 +1171,111 @@ async def get_item_image( } +@app.get("/web/api/inventory/item-detail/{inventory_id}") +async def api_item_detail( + inventory_id: int, + db: Session = Depends(get_db), + user: User = Depends(get_current_user) +): + """Детальная информация о предмете с коллекцией""" + inv_item = db.query(InventoryItem).filter( + InventoryItem.id == inventory_id, + InventoryItem.user_id == user.id + ).first() + if not inv_item: + # Also allow viewing if item exists (public profiles) + inv_item = db.query(InventoryItem).filter( + InventoryItem.id == inventory_id + ).first() + if not inv_item: + return JSONResponse(status_code=404, content={"error": "Предмет не найден"}) + + item_data = get_item(inv_item.item_id) + if not item_data: + return JSONResponse(status_code=404, content={"error": "Данные предмета не найдены"}) + + collection = item_data.get("collection", "").strip() + # fallback to case name + if not collection or collection == "Без коллекции": + crates = item_data.get("crates_names", []) + if crates: + collection = f"Case: {crates[0]}" + + item_resp = { + "id": inv_item.id, + "item_id": inv_item.item_id, + "name": inv_item.market_hash_name, + "rarity": inv_item.rarity, + "wear": inv_item.wear, + "float": inv_item.float_value, + "type": inv_item.type, + "price_rub": item_data.get("price_rub", 100), + "image_url": item_data.get("image_url", ""), + "collection": collection, + "weapon": item_data.get("weapon", ""), + "pattern": item_data.get("pattern", ""), + } + + # Собираем все предметы коллекции + coll_items = [] + all_owned = True + owned_names = set() + + # Получаем все item_id из коллекции + from backend import COLLECTIONS_INDEX, extract_base_name, RARITY_ORDER + collection_ids = COLLECTIONS_INDEX.get(collection, []) + + # Группируем по базовому имени (без учёта вариантов) + seen_base = {} + for cid in collection_ids: + citem = get_item(cid) + if not citem: + continue + base = extract_base_name(citem.get("market_hash_name", "")) + if base and base not in seen_base: + seen_base[base] = citem + + # Все предметы пользователя — для проверки владения + user_items = db.query(InventoryItem.item_id).filter( + InventoryItem.user_id == user.id + ).all() + user_item_ids = {iid for (iid,) in user_items} + + # Сортируем по редкости (низшая → высшая) + sorted_bases = sorted(seen_base.values(), key=lambda x: RARITY_ORDER.get(x.get("rarity", "Unknown"), -1)) + + for citem in sorted_bases: + base = extract_base_name(citem.get("market_hash_name", "")) + owned = False + for u_id in user_item_ids: + u_item = get_item(u_id) + if u_item and extract_base_name(u_item.get("market_hash_name", "")) == base: + owned = True + owned_names.add(base) + break + + if not owned: + all_owned = False + + coll_items.append({ + "base_name": base, + "market_hash_name": citem.get("market_hash_name", ""), + "weapon": citem.get("weapon", ""), + "pattern": citem.get("pattern", ""), + "rarity": citem.get("rarity", ""), + "rarity_level": RARITY_ORDER.get(citem.get("rarity", "Unknown"), -1), + "owned": owned, + "image_url": citem.get("image_url", ""), + }) + + return { + "item": item_resp, + "collection_name": collection if collection else "", + "items": coll_items, + "all_owned": all_owned, + } + + def get_case_items(case_name: str) -> List[dict]: """Получает предметы кейса по его имени""" items = [] @@ -1472,8 +1600,7 @@ async def case_detail_page( "image_url": item["image_url"] }) - return templates.TemplateResponse("case_detail.html", { - "request": request, + return templates.TemplateResponse(request, "case_detail.html", { "user": user, "case_name": case_name, "case_display_name": case_config.get('display_name', case_name), @@ -1885,8 +2012,11 @@ async def api_open_slot_case( # Проверка достижений для слота unlocked_ach = [] if match: - unlocked_ach += check_slot_match_achievement(db, user.id) + unlocked_ach += check_slot_match_achievements(db, user.id) unlocked_ach += check_slot_plays_achievements(db, user.id) + unlocked_ach += check_slot_match_count_achievements(db, user.id) + unlocked_ach += check_balance_achievements(db, user.id) + unlocked_ach += check_achievements_count_achievements(db, user.id) db.commit() @@ -2234,8 +2364,7 @@ async def upgrade_page( "probability": round(u.rpu_adjusted_probability or u.probability or 0, 1) }) - return templates.TemplateResponse("upgrade.html", { - "request": request, + return templates.TemplateResponse(request, "upgrade.html", { "user": user, "inventory_items": enriched_items, "upgrade_rpu": upgrade_rpu, @@ -2450,6 +2579,12 @@ async def api_execute_upgrade( db.commit() unlocked_ach = check_upgrade_achievements(db, user.id) + unlocked_ach += check_upgrade_risk_achievements(db, user.id, input_price) + unlocked_ach += check_upgrade_result_achievements(db, user.id, success, base_probability, input_price) + unlocked_ach += check_dildo_achievements(db, user.id) + unlocked_ach += check_balance_achievements(db, user.id) + unlocked_ach += check_collection_achievements(db, user.id) + unlocked_ach += check_achievements_count_achievements(db, user.id) if success: act = record_activity_own_session(user.id, user.username, "upgrade", @@ -2501,8 +2636,7 @@ async def achievements_page( total = len(achievements) unlocked = sum(1 for a in achievements if a["unlocked"]) - return templates.TemplateResponse("achievements.html", { - "request": request, + return templates.TemplateResponse(request, "achievements.html", { "user": user, "achievements": achievements, "total": total, @@ -2534,8 +2668,7 @@ async def activity_page( ): activities = db.query(ActivityFeed).order_by(desc(ActivityFeed.created_at)).limit(50).all() - return templates.TemplateResponse("activity.html", { - "request": request, + return templates.TemplateResponse(request, "activity.html", { "user": user, "activities": [ { @@ -2681,8 +2814,7 @@ async def crash_page( request: Request, user: User = Depends(get_current_user) ): - return templates.TemplateResponse("crash.html", { - "request": request, + return templates.TemplateResponse(request, "crash.html", { "user": user, "active_page": "crash" }) @@ -2711,11 +2843,9 @@ async def api_crash_bet( CrActivity.user_id == user.id, CrActivity.activity_type == "crash" ).count() + 1 - unlocked_ach = [] - r1 = check_achievement(db, user.id, "crash_first", 1) - if r1: unlocked_ach.append(r1) - r2 = check_achievement(db, user.id, "crash_pro", crash_count) - if r2: unlocked_ach.append(r2) + unlocked_ach = check_crash_achievements(db, user.id, crash_count) + unlocked_ach += check_balance_achievements(db, user.id) + unlocked_ach += check_achievements_count_achievements(db, user.id) db.commit() asyncio.ensure_future(manager.send_personal(user.id, {"type": "balance_update", "balance": user.balance})) @@ -2748,10 +2878,9 @@ async def api_crash_cashout( user.balance += payout # Проверка достижений - unlocked_ach = [] - if g.current_multiplier >= 5.0: - r = check_achievement(db, user.id, "crash_5x", 1) - if r: unlocked_ach.append(r) + unlocked_ach = check_crash_cashout_achievements(db, user.id, g.current_multiplier) + unlocked_ach += check_balance_achievements(db, user.id) + unlocked_ach += check_achievements_count_achievements(db, user.id) db.commit() diff --git a/remote-deploy.sh b/remote-deploy.sh index fad62bf..b1499e7 100755 --- a/remote-deploy.sh +++ b/remote-deploy.sh @@ -12,5 +12,5 @@ rsync -av \ /root/dodep-simulator/ \ root@zernmc.ru:/root/dodep/ -ssh root@zernmc.ru "pkill -f 'uvicorn' 2>/dev/null; pkill -f 'python3.*backend' 2>/dev/null; sleep 2; cd /root/dodep && rm -rf __pycache__ && setsid python3 -m uvicorn frontend:app --host 0.0.0.0 --port 13669 > /tmp/frontend.log 2>&1 &" +ssh root@zernmc.ru "cd /root/dodep && pip install --break-system-packages -r requirements.txt 2>&1 | tail -5 && pkill -f 'uvicorn' 2>/dev/null; pkill -f 'python3.*backend' 2>/dev/null; sleep 2; rm -rf __pycache__ && setsid python3 -m uvicorn frontend:app --host 0.0.0.0 --port 13669 > /tmp/frontend.log 2>&1 &" echo "Deployed and restarted." diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bb70430 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +fastapi>=0.139.0 +uvicorn>=0.50.0 +starlette>=1.3.1 +jinja2>=3.1.6 +markupsafe>=3.0.0 +sqlalchemy>=2.0 +python-jose[cryptography] +passlib +bcrypt +python-multipart +python-dateutil +aiofiles +tqdm +aiosqlite diff --git a/templates/achievements.html b/templates/achievements.html index 9bf5a14..ed80fc6 100644 --- a/templates/achievements.html +++ b/templates/achievements.html @@ -6,6 +6,30 @@ Достижения - CS2 Simulator @@ -965,7 +1088,104 @@ const btn = document.getElementById('soundToggle'); if (btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇'; }); + + // ─── Карточка предмета (попап) ───────────────────────────────────── + document.addEventListener('DOMContentLoaded', function() { + const grid = document.getElementById('inventoryGrid'); + if (!grid) return; + grid.addEventListener('click', function(e) { + const card = e.target.closest('.inventory-skin-card'); + if (!card) return; + if (e.target.closest('.item-select-checkbox, .btn-sell-single')) return; + const invId = card.dataset.inventoryId; + if (!invId) return; + openItemCard(invId); + }); + }); + + async function openItemCard(invId) { + const modal = document.getElementById('itemCardModal'); + const overlay = document.getElementById('itemCardOverlay'); + overlay.style.display = 'flex'; + modal.innerHTML = '
Загрузка...
'; + try { + const res = await fetch('/web/api/inventory/item-detail/' + invId); + if (!res.ok) throw new Error('Ошибка загрузки'); + const data = await res.json(); + renderItemCard(data); + } catch(e) { + modal.innerHTML = '
Ошибка загрузки данных
'; + } + } + + function renderItemCard(data) { + const modal = document.getElementById('itemCardModal'); + const item = data.item; + const colName = data.collection_name || 'Без коллекции'; + const rc = (item.rarity || '').toLowerCase().replace(/\s+/g, '-'); + const rarityColor = getRarityColor(item.rarity) || '#b0b0b0'; + const img = item.image_url || '/static/placeholder.png'; + + let collHtml = ''; + if (data.items && data.items.length) { + data.items.forEach(function(ci) { + const ciRc = (ci.rarity || '').toLowerCase().replace(/\s+/g, '-'); + const ciColor = getRarityColor(ci.rarity) || '#b0b0b0'; + let statusColor = ci.owned ? '#ffffff' : '#555'; + if (data.all_owned) statusColor = '#ffd700'; + collHtml += '
' + + '' + ci.rarity + '' + + ' ' + ci.market_hash_name + '' + + (ci.owned ? ' ' : '') + + '
'; + }); + } + + modal.innerHTML = '
' + + '
' + + '
' + + '' + item.name + '' + + '
' + + '
' + + '
' + item.name + '
' + + '
Коллекция' + colName + '
' + + '
Поношенность' + item.wear + '
' + + '
Float' + (item.float != null ? item.float.toFixed(6) : '—') + '
' + + '
Редкость' + item.rarity + '
' + + '
Тип' + (item.type || 'Normal') + '
' + + '
Цена💰 ' + (item.price_rub || 0).toLocaleString() + ' ₽
' + + (collHtml ? '
🎨 Скины коллекции
' + collHtml + '
' : '') + + '' + + '
'; + } + + function closeItemCard() { + document.getElementById('itemCardOverlay').style.display = 'none'; + } + + function getRarityColor(rarity) { + const colors = { + 'Consumer Grade': '#b0b0b0', + 'Industrial Grade': '#5e98d9', + 'Mil-Spec': '#4b69ff', + 'Mil-Spec Grade': '#4b69ff', + 'Restricted': '#8847ff', + 'Classified': '#d32ce6', + 'Covert': '#eb4b4b', + 'Rare Special Item': '#ffd700', + 'Extraordinary': '#ffd700', + 'Contraband': '#ffaa00' + }; + return colors[rarity] || '#b0b0b0'; + } + + // ─── Попап карточки предмета (HTML в body) ───────────────────────── + + + {% if not is_public %}{% include '_activity_sidebar.html' %}{% endif %} diff --git a/templates/upgrade.html b/templates/upgrade.html index 0c754ec..80acdbf 100644 --- a/templates/upgrade.html +++ b/templates/upgrade.html @@ -757,6 +757,7 @@ +