Gamification
Система XP и streaks для мотивации.
Структура
src/features/gamification/
├── api/
│ ├── queries.ts # fetchUserGamification()
│ └── mutations.ts # awardXp()
├── hooks/
│ ├── use-gamification.ts # Получить XP, streaks
│ └── use-xp-award.ts # Добавить XP
├── types.ts
└── index.ts
Доменная модель
UserGamification
├── id: string
├── userId: string
├── totalXp: number
├── currentStreak: number
├── longestStreak: number
├── lastActivityDate: Date
├── level: number
└── achievements: Achievement[]
XP система
Начисление XP
| Действие | XP |
|---|---|
| Завершить урок | 50 |
| Завершить модуль | 100 |
| Завершить курс | 500 |
| Игра Memory Cards | 10-50 |
| Игра Pattern Sequence | 10-50 |
| Пройти VARK тест | 25 |
| Streak бонус (за день) | 5 × days |
Уровни
// Формула уровня
function calculateLevel(totalXp: number): number {
return Math.floor(Math.sqrt(totalXp / 100)) + 1;
}
// XP до следующего уровня
function xpToNextLevel(totalXp: number): number {
const currentLevel = calculateLevel(totalXp);
const xpForCurrentLevel = Math.pow(currentLevel - 1, 2) * 100;
const xpForNextLevel = Math.pow(currentLevel, 2) * 100;
return xpForNextLevel - totalXp;
}
| Уровень | Требуется XP |
|---|---|
| 1 | 0 |
| 2 | 100 |
| 3 | 400 |
| 4 | 900 |
| 5 | 1600 |
| ... | n² × 100 |
Hooks
useGamification
import { useGamification } from '@/features/gamification';
function ProfileScreen() {
const { data: gamification } = useGamification();
return (
<View>
<Text>XP: {gamification?.totalXp}</Text>
<Text>Уровень: {gamification?.level}</Text>
<Text>Streak: {gamification?.currentStreak} дней</Text>
</View>
);
}
useXpAward
import { useXpAward } from '@/features/gamification';
function LessonCompleteScreen({ lessonId }) {
const { mutate: awardXp } = useXpAward();
useEffect(() => {
awardXp({
action: 'LESSON_COMPLETE',
lessonId,
xp: 50,
});
}, []);
return <SuccessAnimation />;
}
Streaks
Механика
- Каждый день активности (урок, игра) увеличивает streak
- Пропуск дня сбрасывает streak на 0
- Бонус XP = 5 × currentStreak за каждый день
// api/mutations.ts
export async function updateStreak(userId: string) {
const { data: gamification } = await supabase
.from('user_gamification')
.select('*')
.eq('user_id', userId)
.single();
const today = new Date().toDateString();
const lastActivity = new Date(gamification.lastActivityDate).toDateString();
if (today === lastActivity) {
// Уже обновлено сегодня
return gamification;
}
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const isConsecutive = lastActivity === yesterday.toDateString();
const newStreak = isConsecutive ? gamification.currentStreak + 1 : 1;
const streakBonus = newStreak * 5;
await supabase
.from('user_gamification')
.update({
currentStreak: newStreak,
longestStreak: Math.max(newStreak, gamification.longestStreak),
lastActivityDate: new Date(),
totalXp: gamification.totalXp + streakBonus,
})
.eq('user_id', userId);
}
Напоминания
См. Notifications — напоминания для сохранения streak.
Компоненты
StatsCard
function StatsCard() {
const { data } = useGamification();
return (
<View className="bg-card rounded-xl p-4">
<View className="flex-row justify-between">
<StatItem
icon="flame"
value={data?.currentStreak || 0}
label="дней подряд"
/>
<StatItem
icon="star"
value={data?.totalXp || 0}
label="XP"
/>
<StatItem
icon="trophy"
value={data?.level || 1}
label="уровень"
/>
</View>
</View>
);
}
XpAnimation
Анимация при получении XP:
function XpAnimation({ amount, onComplete }) {
const scale = useSharedValue(0);
const opacity = useSharedValue(1);
useEffect(() => {
scale.value = withSpring(1);
opacity.value = withDelay(1000, withTiming(0, { duration: 500 }));
setTimeout(onComplete, 1500);
}, []);
return (
<Animated.View style={[animatedStyle]}>
<Text className="text-2xl font-bold text-accent">+{amount} XP</Text>
</Animated.View>
);
}
Query Keys
['gamification', userId] // Данные геймификации пользователя
['gamification', 'leaderboard'] // Таблица лидеров (если есть)