Перейти к основному содержимому

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 Cards10-50
Игра Pattern Sequence10-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
10
2100
3400
4900
51600
...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

Механика

  1. Каждый день активности (урок, игра) увеличивает streak
  2. Пропуск дня сбрасывает streak на 0
  3. Бонус 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'] // Таблица лидеров (если есть)