from datetime import timedelta from django.db import models from django.utils import timezone HEART_REFILL_INTERVAL = timedelta(hours=24) class UserStats(models.Model): user_id = models.IntegerField(unique=True, db_index=True) # ارجاع سبک به accounts.User xp = models.PositiveIntegerField(default=0) coins = models.PositiveIntegerField(default=0) progress_percent = models.FloatField(default=0) # ارجاع سبک به Character؛ کاراکترِ انتخابیِ کاربر (مثلِ ماسکاتِ Duolingo). # می‌تونه به کاراکترِ هنوز باز‌نشده اشاره نکنه — CharacterSelectView قبل از # ست‌کردن چک می‌کنه unlock_xp <= xp. selected_character_id = models.IntegerField(null=True, blank=True) def __str__(self): return f"stats-user{self.user_id}" class LeaderboardEntry(models.Model): PERIOD_CHOICES = [('weekly', 'هفتگی'), ('all_time', 'کل زمان')] user_id = models.IntegerField(db_index=True) period = models.CharField(max_length=10, choices=PERIOD_CHOICES) xp_in_period = models.PositiveIntegerField(default=0) rank = models.PositiveIntegerField() class Meta: ordering = ['rank'] def __str__(self): return f"lb-{self.period}-user{self.user_id}-rank{self.rank}" class StreakRecord(models.Model): user_id = models.IntegerField(unique=True, db_index=True) current_streak = models.PositiveIntegerField(default=0) longest_streak = models.PositiveIntegerField(default=0) def __str__(self): return f"streak-user{self.user_id}" class ActiveDay(models.Model): streak = models.ForeignKey(StreakRecord, on_delete=models.CASCADE, related_name='active_days') date = models.DateField() class Meta: ordering = ['date'] def __str__(self): return str(self.date) class UserHearts(models.Model): user_id = models.IntegerField(unique=True, db_index=True) current_hearts = models.PositiveSmallIntegerField(default=5) max_hearts = models.PositiveSmallIntegerField(default=5) next_refill_at = models.DateTimeField(null=True, blank=True) def __str__(self): return f"hearts-user{self.user_id}" def apply_regen(self): """اگه از next_refill_at گذشته باشه، هارت(های) گذشته رو پر می‌کنه (چندتا هم اگه کاربر چند بازه‌ی HEART_REFILL_INTERVAL غایب بوده باشه). True یعنی state عوض شده و باید save بشه.""" if self.current_hearts >= self.max_hearts: if self.next_refill_at is not None: self.next_refill_at = None return True return False now = timezone.now() if self.next_refill_at is None: self.next_refill_at = now + HEART_REFILL_INTERVAL return True if now < self.next_refill_at: return False elapsed_intervals = 1 + (now - self.next_refill_at) // HEART_REFILL_INTERVAL gained = min(elapsed_intervals, self.max_hearts - self.current_hearts) self.current_hearts += gained if self.current_hearts >= self.max_hearts: self.next_refill_at = None else: self.next_refill_at += elapsed_intervals * HEART_REFILL_INTERVAL return True def lose_heart(self): """یک هارت کم می‌کنه؛ اگه تایمرِ refill از قبل در حال شمارش نیست، از الان شروعش می‌کنه.""" if self.current_hearts > 0: self.current_hearts -= 1 if self.next_refill_at is None and self.current_hearts < self.max_hearts: self.next_refill_at = timezone.now() + HEART_REFILL_INTERVAL class Character(models.Model): """ کاراکترهای گیمیفیکیشن (مثلِ ماسکات‌های Duolingo) — یک کاتالوگِ ثابت که از پنلِ ادمین مدیریت می‌شه (دقیقاً مثلِ subscription.Plan)، نه یک چیزی که کاربر خودش می‌سازه. فعلاً باز/بسته‌بودن فقط تابعی از XP کاربره (بدونِ جدولِ جداگانه‌ی مالکیت)؛ اگه بعداً «خریدن با coin» یا شرطِ دیگه‌ای لازم شد، همین‌جا اضافه می‌شه. """ slug = models.SlugField(max_length=50, unique=True) name = models.CharField(max_length=50) # کلیدی که فرانت به تصویر/انیمیشنِ واقعی نگاشت می‌ده — همون الگویِ # content.Lesson.mascot_animation و accounts.User.avatar؛ فعلاً آپلودِ # asset واقعی نداریم. icon = models.CharField(max_length=100) unlock_xp = models.PositiveIntegerField(default=0) order = models.PositiveIntegerField(default=0) class Meta: ordering = ['order', 'unlock_xp'] def __str__(self): return self.name