from datetime import timedelta from django.db import models from django.utils import timezone # مثلِ دولینگو: هر ۵ ساعت یک قلب برمی‌گردد، و ۲۴ ساعت بعد از اولین قلبی که # از دست رفت همه یک‌جا پر می‌شوند (تصمیمِ صاحبِ پروژه، ۲۰۲۶-۰۹-۲۴). قبلاً هر # قلب ۲۴ ساعت بود؛ دانش‌آموزی که هر پنج را از دست می‌داد تا پنج روز می‌ماند. HEART_REFILL_INTERVAL = timedelta(hours=5) HEART_FULL_REFILL_AFTER = 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) # قلبِ بعدی کی برمی‌گردد (هر HEART_REFILL_INTERVAL یکی). next_refill_at = models.DateTimeField(null=True, blank=True) # همه‌ی قلب‌ها کی یک‌جا پر می‌شوند: HEART_FULL_REFILL_AFTER بعد از اولین # قلبی که از حالتِ پر کم شد. full_refill_at = models.DateTimeField(null=True, blank=True) REGEN_FIELDS = ['current_hearts', 'next_refill_at', 'full_refill_at'] def __str__(self): return f"hearts-user{self.user_id}" def _refill_all(self): self.current_hearts = self.max_hearts self.next_refill_at = None self.full_refill_at = None def apply_regen(self): """ قلب‌هایی را که وقتشان رسیده برمی‌گرداند. True یعنی state عوض شده و باید با REGEN_FIELDS ذخیره شود. ۱) اگر از full_refill_at گذشته باشد، همه یک‌جا پر می‌شوند. ۲) وگرنه به ازای هر HEART_REFILL_INTERVAL که از next_refill_at گذشته، یک قلب (چندتا اگر کاربر مدتی نبوده). """ if self.current_hearts >= self.max_hearts: if self.next_refill_at is not None or self.full_refill_at is not None: self.next_refill_at = None self.full_refill_at = None return True return False now = timezone.now() changed = False if self.full_refill_at is None: self.full_refill_at = now + HEART_FULL_REFILL_AFTER changed = True if self.next_refill_at is None or self.next_refill_at > now + HEART_REFILL_INTERVAL: # نبودِ تایمر، یا تایمرِ ۲۴ساعته‌ی قدیمی از پیش از این تغییر. self.next_refill_at = now + HEART_REFILL_INTERVAL changed = True if now >= self.full_refill_at: self._refill_all() return True if now < self.next_refill_at: return changed 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._refill_all() else: self.next_refill_at += elapsed_intervals * HEART_REFILL_INTERVAL return True def lose_heart(self): """یک قلب کم می‌کند و اگر تایمرها در حالِ شمارش نیستند، از الان شروعشان می‌کند.""" if self.current_hearts > 0: self.current_hearts -= 1 if self.current_hearts < self.max_hearts: now = timezone.now() if self.next_refill_at is None: self.next_refill_at = now + HEART_REFILL_INTERVAL if self.full_refill_at is None: self.full_refill_at = now + HEART_FULL_REFILL_AFTER 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