Sistema de Gamificação - Sport Tech Club
1. Fundamentos de Neurociência
1.1 Motivação Intrínseca
Baseado na Teoria da Autodeterminação (Deci & Ryan):
Autonomia
- Jogador escolhe quando jogar
- Customização de perfil e avatar
- Decisão sobre participar de desafios
- Controle sobre privacidade de dados
Maestria
- Sistema de progressão visível
- Feedback de evolução técnica
- Metas graduais e alcançáveis
- Reconhecimento de melhoria
Pertencimento
- Comunidade de jogadores
- Times e grupos
- Reconhecimento social
- Conexões significativas
1.2 Sistema de Recompensa (Dopamina)
Ciclo de Dopamina Saudável
Antecipação → Ação → Recompensa → Consolidação
↑ ↓
←────────────────────────────────────Triggers de Dopamina
- XP ganho (recompensa imediata)
- Level up (marco significativo)
- Badge desbloqueado (conquista)
- Vitória em partida (resultado)
- Streak mantido (consistência)
1.3 Recompensas Variáveis
Schedule de Recompensas
- Fixed Ratio: XP por partida (previsível)
- Variable Ratio: Badges raros (imprevisível)
- Fixed Interval: Desafios semanais
- Variable Interval: Eventos surpresa
Loot Box Ético
- Transparência de probabilidades
- Sem pay-to-win
- Apenas itens cosméticos
- Sempre obtível por jogo
1.4 Redução de Fricção Cognitiva
Princípios de UX
- Onboarding em 3 passos
- Tutorial interativo
- Defaults inteligentes
- Ações com 1 clique
- Informação progressiva
Flow State
- Desafio balanceado com habilidade
- Feedback imediato
- Metas claras
- Sem distrações
1.5 Feedback Rápido e Visual
Feedback Imediato
- Animações de XP (+50 XP)
- Progress bars animadas
- Confetti em conquistas
- Notificações push
- Sound effects
Feedback Visual
interface FeedbackEvent {
type: 'xp' | 'level' | 'badge' | 'streak';
animation: 'pulse' | 'confetti' | 'glow';
duration: number;
sound?: string;
}1.6 Social Proof
Elementos Sociais
- "X jogadores online agora"
- "Seus amigos jogaram Y partidas"
- "Top 10% dos jogadores"
- Testemunhos de conquistas
- Feed de atividades
FOMO Positivo
- "Desafio termina em 2h"
- "Evento especial hoje"
- Sem punição por perder
1.7 Microvitórias
Wins Frequentes
- +10 XP por check-in
- +25 XP por partida completa
- +50 XP por vitória
- Primeiro badge em 5min
- Nível 2 em primeira sessão
Celebração Apropriada
const celebrationLevel = {
micro: 'subtle_animation', // +10 XP
small: 'notification_toast', // Badge comum
medium: 'modal_celebration', // Level up
large: 'full_screen_confetti' // Badge épico
};1.8 Estímulo à Recorrência
Habit Building
- Streak de dias consecutivos
- Desafios diários (3 tarefas)
- Recompensa de login diário
- Notificações inteligentes
Optimal Timing
const notificationSchedule = {
daily: '19:00', // Horário típico de jogo
weekly: 'Sexta 18:00', // Fim de semana
abandoned: '3 dias', // Re-engagement
achievement: 'instant' // Celebração
};1.9 Design para Dopamina Saudável
Evitar Dark Patterns
❌ NÃO fazer:
- Infinite scroll sem break
- Recompensas manipulativas
- Pay-to-win
- FOMO punitivo
- Fake scarcity
- Shame por não jogar
✅ FAZER:
- Limites saudáveis sugeridos
- Transparência total
- Recompensas justas
- FOMO positivo
- Pausa sugerida após 2h
- Encorajamento positivo
Health Metrics
interface HealthyEngagement {
maxSessionTime: 120; // minutos
suggestBreakAfter: 90;
cooldownPeriod: 30;
maxDailyNotifications: 3;
respectDoNotDisturb: true;
}2. Mecânicas de Gamificação
2.1 Sistema de XP (Experience Points)
Ganho de XP
| Ação | XP Base | Multiplicadores |
|---|---|---|
| Check-in na arena | 10 | - |
| Partida completa | 25 | +50% se vitória |
| Vitória | 50 | +25% se streak |
| MVP da partida | 100 | - |
| Primeiro jogo do dia | 20 | Bonus diário |
| Desafio completado | 100-500 | Por dificuldade |
| Evento especial | 200+ | Variável |
| Streak de 7 dias | 500 | Semanal |
| Convidar amigo | 150 | Por aceite |
Fórmula de XP
function calculateXP(action: Action, context: Context): number {
let xp = BASE_XP[action.type];
// Multiplicadores
if (context.isVictory) xp *= 1.5;
if (context.hasStreak) xp *= 1.25;
if (context.isFirstToday) xp += 20;
if (context.isMVP) xp *= 2;
// Bônus de nível (reduz grind)
if (context.playerLevel < 10) xp *= 1.5;
return Math.floor(xp);
}2.2 Sistema de Níveis
Progressão de Níveis
| Nível | XP Total | XP Necessário | Título | Benefício |
|---|---|---|---|---|
| 1 | 0 | 0 | Novato | - |
| 2 | 100 | 100 | Iniciante | Avatar frame bronze |
| 3 | 250 | 150 | Aprendiz | +1 desafio diário |
| 5 | 750 | 500 | Jogador | Badge customizado |
| 10 | 3000 | 2250 | Experiente | Highlight button |
| 15 | 7500 | 4500 | Veterano | Partida "valendo" |
| 20 | 15000 | 7500 | Expert | Avatar frame prata |
| 30 | 35000 | 20000 | Mestre | Criar torneios |
| 40 | 65000 | 30000 | Campeão | Avatar frame ouro |
| 50 | 100000 | 35000 | Lenda | Avatar frame diamante |
Curva de Progressão
function xpForLevel(level: number): number {
// Curva logarítmica suavizada
if (level <= 1) return 0;
const base = 100;
const exponent = 1.5;
const smoothing = 0.85;
return Math.floor(
base * Math.pow(level, exponent) * smoothing
);
}
function levelFromXP(totalXP: number): number {
let level = 1;
let accumulatedXP = 0;
while (accumulatedXP <= totalXP) {
level++;
accumulatedXP += xpForLevel(level);
}
return level - 1;
}Visualização de Progresso
interface LevelProgress {
currentLevel: number;
currentXP: number;
xpForCurrentLevel: number;
xpForNextLevel: number;
progressPercent: number;
title: string;
nextTitle: string;
estimatedMatchesToNext: number;
}2.3 Sistema de Badges/Conquistas
Categorias de Badges
A) Participação
- First Steps: Primeira partida
- Regular: 10 partidas
- Dedicated: 50 partidas
- Hardcore: 100 partidas
- Legend: 500 partidas
- Marathon: 10 partidas em 1 dia
- Weekend Warrior: 20 partidas em fim de semana
B) Performance
- Hat Trick: 3 gols em 1 partida (futebol)
- Triple Double: 10+ pontos, 10+ rebotes, 10+ assistências (basquete)
- Perfect Game: Vitória sem sofrer pontos
- Comeback King: Vitória após perder por 10+ pontos
- Domination: Vitória por 20+ pontos
- Sharpshooter: 80%+ de aproveitamento
C) Social
- Team Player: 10 partidas com mesmo time
- Ambassador: Convidar 5 amigos
- Community Hero: 50 likes recebidos
- Supporter: 100 check-ins como torcedor
- Organizer: Criar 10 partidas
D) Streak
- On Fire: 3 vitórias seguidas
- Unstoppable: 5 vitórias seguidas
- Legendary Streak: 10 vitórias seguidas
- Dedicated: 7 dias consecutivos
- Month Warrior: 30 dias consecutivos
E) Evolução
- Rising Star: Level 10
- Veteran: Level 25
- Master: Level 50
- First Win: Primeira vitória
- Improvement: +50% win rate em 30 dias
F) Raros/Épicos
- Unicorn: Vitória com score exato (111 pontos)
- Perfect Week: 7 vitórias em 7 dias
- Grand Slam: Ganhar torneio de 16+ jogadores
- Hall of Fame: Top 10 ranking global
- GOAT: Top 1 ranking global por 30 dias
Schema de Badge
interface Badge {
id: string;
name: string;
description: string;
category: 'participation' | 'performance' | 'social' | 'streak' | 'evolution' | 'rare';
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
icon: string;
xpReward: number;
arenaCoinsReward?: number;
// Condições
requirements: Requirement[];
// Metadata
earnedBy: number; // Quantidade de jogadores
earnedByPercent: number;
firstEarnedBy?: string;
firstEarnedAt?: Date;
}
interface Requirement {
type: 'matches_played' | 'wins' | 'streak' | 'score' | 'level' | 'social';
value: number;
timeframe?: 'daily' | 'weekly' | 'monthly' | 'alltime';
}2.4 Sistema de Rankings
Tipos de Rankings
enum RankingType {
GLOBAL = 'global',
SPORT = 'sport', // Por modalidade
ARENA = 'arena', // Por local
AGE_GROUP = 'age_group', // Por faixa etária
GENDER = 'gender', // Por gênero
LEVEL = 'level', // Por nível similar
FRIENDS = 'friends', // Entre amigos
TEAM = 'team' // Times
}
enum RankingPeriod {
DAILY = 'daily',
WEEKLY = 'weekly',
MONTHLY = 'monthly',
SEASON = 'season',
ALLTIME = 'alltime'
}Cálculo de Rating
Baseado em Elo Rating adaptado:
interface PlayerRating {
overall: number; // Rating geral
bySport: Map<string, number>; // Por esporte
wins: number;
losses: number;
winRate: number;
averageScore: number;
matchesPlayed: number;
// Tendência
trend: 'rising' | 'stable' | 'falling';
trendPercent: number;
// Ranking
globalRank: number;
sportRank: number;
percentile: number;
}
function calculateNewRating(
playerRating: number,
opponentRating: number,
result: 'win' | 'loss' | 'draw',
kFactor: number = 32
): number {
const expectedScore = 1 / (1 + Math.pow(10, (opponentRating - playerRating) / 400));
const actualScore = result === 'win' ? 1 : result === 'loss' ? 0 : 0.5;
const newRating = playerRating + kFactor * (actualScore - expectedScore);
return Math.round(newRating);
}Leaderboard UI
interface LeaderboardEntry {
rank: number;
previousRank: number;
player: {
id: string;
name: string;
avatar: string;
level: number;
};
rating: number;
ratingChange: number;
wins: number;
losses: number;
winRate: number;
badges: Badge[];
isCurrentUser?: boolean;
}
interface Leaderboard {
type: RankingType;
period: RankingPeriod;
sport?: string;
topPlayers: LeaderboardEntry[]; // Top 10
nearbyPlayers: LeaderboardEntry[]; // 5 acima + user + 5 abaixo
userEntry: LeaderboardEntry;
totalPlayers: number;
lastUpdated: Date;
}2.5 Sistema de Desafios
Tipos de Desafios
enum ChallengeType {
DAILY = 'daily', // Renovado diariamente
WEEKLY = 'weekly', // Renovado semanalmente
SEASONAL = 'seasonal', // Por temporada
EVENT = 'event', // Eventos especiais
PERSONAL = 'personal', // Baseado em histórico
COMMUNITY = 'community' // Desafio global
}
interface Challenge {
id: string;
type: ChallengeType;
name: string;
description: string;
// Objetivos
objectives: Objective[];
// Recompensas
xpReward: number;
arenaCoinsReward: number;
badgeReward?: string;
// Timing
startsAt: Date;
endsAt: Date;
// Progresso
progress: number;
progressMax: number;
isCompleted: boolean;
// Dificuldade
difficulty: 'easy' | 'medium' | 'hard' | 'expert';
// Social
participantCount: number;
completionRate: number;
}
interface Objective {
id: string;
description: string;
type: 'play_matches' | 'win_matches' | 'score_points' | 'invite_friends' | 'check_in';
target: number;
current: number;
isCompleted: boolean;
}Exemplos de Desafios
const dailyChallenges = [
{
name: "Aquecimento",
objectives: [
{ type: 'play_matches', target: 3 }
],
xpReward: 100,
difficulty: 'easy'
},
{
name: "Tríplice Coroa",
objectives: [
{ type: 'play_matches', target: 3 },
{ type: 'win_matches', target: 2 },
{ type: 'score_points', target: 50 }
],
xpReward: 300,
arenaCoinsReward: 50,
difficulty: 'medium'
},
{
name: "Campeão do Dia",
objectives: [
{ type: 'win_matches', target: 5 },
{ type: 'score_points', target: 100 }
],
xpReward: 500,
arenaCoinsReward: 100,
difficulty: 'hard'
}
];
const weeklyChallenges = [
{
name: "Maratonista",
objectives: [
{ type: 'play_matches', target: 20 }
],
xpReward: 1000,
arenaCoinsReward: 200,
badgeReward: 'weekly_warrior',
difficulty: 'medium'
},
{
name: "Invencível",
objectives: [
{ type: 'win_matches', target: 10 },
{ type: 'play_matches', target: 15 }
],
xpReward: 2000,
arenaCoinsReward: 500,
badgeReward: 'weekly_champion',
difficulty: 'hard'
}
];2.6 Sistema de Streaks
Tipos de Streak
interface Streak {
type: 'login' | 'play' | 'win' | 'challenge';
current: number;
best: number;
lastDate: Date;
// Rewards
milestones: StreakMilestone[];
nextMilestone: StreakMilestone;
// Status
isActive: boolean;
expiresAt: Date;
daysUntilExpire: number;
}
interface StreakMilestone {
days: number;
xpReward: number;
arenaCoinsReward: number;
badgeReward?: string;
title: string;
}
const streakMilestones: StreakMilestone[] = [
{ days: 3, xpReward: 100, arenaCoinsReward: 50, title: "Iniciando" },
{ days: 7, xpReward: 300, arenaCoinsReward: 150, badgeReward: 'week_streak', title: "Dedicado" },
{ days: 14, xpReward: 700, arenaCoinsReward: 350, title: "Consistente" },
{ days: 30, xpReward: 2000, arenaCoinsReward: 1000, badgeReward: 'month_streak', title: "Comprometido" },
{ days: 60, xpReward: 5000, arenaCoinsReward: 2500, title: "Incansável" },
{ days: 100, xpReward: 10000, arenaCoinsReward: 5000, badgeReward: 'century_streak', title: "Lendário" }
];Lógica de Streak
class StreakManager {
checkStreak(userId: string, type: StreakType): Streak {
const streak = this.getStreak(userId, type);
const now = new Date();
const lastAction = streak.lastDate;
const hoursSinceLastAction =
(now.getTime() - lastAction.getTime()) / (1000 * 60 * 60);
// Streak quebrado se passou mais de 48h
if (hoursSinceLastAction > 48) {
return this.resetStreak(streak);
}
// Mesma data, não incrementa
if (this.isSameDay(now, lastAction)) {
return streak;
}
// Incrementa streak
return this.incrementStreak(streak);
}
incrementStreak(streak: Streak): Streak {
streak.current++;
streak.lastDate = new Date();
if (streak.current > streak.best) {
streak.best = streak.current;
}
// Verifica milestones
const milestone = this.checkMilestone(streak);
if (milestone) {
this.rewardMilestone(streak, milestone);
}
return streak;
}
// Freeze de streak (comprado com ArenaCoins)
freezeStreak(streak: Streak, days: number): void {
streak.expiresAt = addDays(new Date(), days + 2); // +2 para margem
}
}3. Sistema de Progressão
3.1 Cálculo de XP por Ação
Engine de XP
class XPEngine {
private baseXPTable = {
CHECK_IN: 10,
MATCH_COMPLETE: 25,
MATCH_WIN: 50,
MATCH_MVP: 100,
CHALLENGE_COMPLETE: 100,
FRIEND_INVITE: 150,
EVENT_PARTICIPATION: 200
};
calculateXP(event: GameEvent, player: Player): XPGain {
let xp = this.baseXPTable[event.type] || 0;
const multipliers: Multiplier[] = [];
// 1. Multiplicador de vitória
if (event.isVictory) {
xp *= 1.5;
multipliers.push({ type: 'victory', value: 1.5 });
}
// 2. Multiplicador de streak
if (player.hasActiveStreak) {
xp *= 1.25;
multipliers.push({ type: 'streak', value: 1.25 });
}
// 3. Bônus primeiro jogo do dia
if (event.isFirstToday) {
xp += 20;
multipliers.push({ type: 'daily_bonus', value: 20 });
}
// 4. Boost para novatos (anti-grind inicial)
if (player.level < 10) {
xp *= 1.5;
multipliers.push({ type: 'newbie_boost', value: 1.5 });
}
// 5. Evento especial
if (event.isSpecialEvent) {
xp *= 2;
multipliers.push({ type: 'special_event', value: 2 });
}
// 6. Performance excepcional
if (event.performance === 'exceptional') {
xp *= 1.5;
multipliers.push({ type: 'exceptional', value: 1.5 });
}
return {
baseXP: this.baseXPTable[event.type],
finalXP: Math.floor(xp),
multipliers,
breakdown: this.generateBreakdown(xp, multipliers)
};
}
}
interface XPGain {
baseXP: number;
finalXP: number;
multipliers: Multiplier[];
breakdown: string;
}3.2 Tabela de Níveis Completa
const LEVEL_TABLE = [
{ level: 1, xpRequired: 0, totalXP: 0, title: "Novato" },
{ level: 2, xpRequired: 100, totalXP: 100, title: "Iniciante" },
{ level: 3, xpRequired: 150, totalXP: 250, title: "Aprendiz" },
{ level: 4, xpRequired: 200, totalXP: 450, title: "Jogador" },
{ level: 5, xpRequired: 300, totalXP: 750, title: "Jogador" },
{ level: 6, xpRequired: 400, totalXP: 1150, title: "Competidor" },
{ level: 7, xpRequired: 500, totalXP: 1650, title: "Competidor" },
{ level: 8, xpRequired: 600, totalXP: 2250, title: "Atleta" },
{ level: 9, xpRequired: 700, totalXP: 2950, title: "Atleta" },
{ level: 10, xpRequired: 750, totalXP: 3700, title: "Experiente" },
{ level: 15, xpRequired: 3800, totalXP: 7500, title: "Veterano" },
{ level: 20, xpRequired: 7500, totalXP: 15000, title: "Expert" },
{ level: 25, xpRequired: 10000, totalXP: 25000, title: "Elite" },
{ level: 30, xpRequired: 10000, totalXP: 35000, title: "Mestre" },
{ level: 40, xpRequired: 30000, totalXP: 65000, title: "Campeão" },
{ level: 50, xpRequired: 35000, totalXP: 100000, title: "Lenda" },
{ level: 60, xpRequired: 50000, totalXP: 150000, title: "Imortal" },
{ level: 70, xpRequired: 75000, totalXP: 225000, title: "Divino" },
{ level: 80, xpRequired: 100000, totalXP: 325000, title: "Mítico" },
{ level: 90, xpRequired: 125000, totalXP: 450000, title: "GOAT" },
{ level: 100, xpRequired: 150000, totalXP: 600000, title: "Hall of Fame" }
];3.3 Habilidades Evoluíveis
interface Skill {
id: string;
name: string;
sport: string;
category: 'offense' | 'defense' | 'strategy' | 'physical' | 'mental';
currentLevel: number;
maxLevel: number;
experience: number;
experienceToNext: number;
description: string;
benefits: string[];
unlockedAt: number; // Player level requirement
}
// Exemplo: Futebol
const footballSkills: Skill[] = [
{
id: 'shooting',
name: 'Finalização',
sport: 'football',
category: 'offense',
maxLevel: 10,
description: 'Melhora precisão e potência dos chutes',
benefits: [
'Nível 3: +5% precisão',
'Nível 5: Chutes curvados',
'Nível 10: Finalização de elite'
],
unlockedAt: 1
},
{
id: 'dribbling',
name: 'Drible',
sport: 'football',
category: 'offense',
maxLevel: 10,
description: 'Controle de bola e capacidade de driblar',
benefits: [
'Nível 3: +10% controle',
'Nível 7: Dribles especiais',
'Nível 10: Mestre do drible'
],
unlockedAt: 5
}
];
class SkillProgression {
addExperience(skill: Skill, amount: number): SkillLevelUp | null {
skill.experience += amount;
if (skill.experience >= skill.experienceToNext) {
return this.levelUpSkill(skill);
}
return null;
}
levelUpSkill(skill: Skill): SkillLevelUp {
skill.currentLevel++;
skill.experience -= skill.experienceToNext;
skill.experienceToNext = this.calculateNextLevelXP(skill.currentLevel);
return {
skillId: skill.id,
newLevel: skill.currentLevel,
unlockedBenefit: this.getUnlockedBenefit(skill)
};
}
}3.4 Progressão Visual
interface ProgressionVisual {
// Level Progress
levelProgress: {
currentLevel: number;
nextLevel: number;
currentXP: number;
requiredXP: number;
percentComplete: number;
animation: 'pulse' | 'glow' | 'fill';
};
// Visual Assets
avatarFrame: {
tier: 'bronze' | 'silver' | 'gold' | 'platinum' | 'diamond';
animation: boolean;
glow: string; // Hex color
};
// Title Display
title: {
current: string;
color: string;
icon?: string;
};
// Stats Cards
stats: {
totalMatches: number;
winRate: number;
favoriteSport: string;
hoursPlayed: number;
rank: number;
percentile: number;
};
// Achievements Showcase
showcaseBadges: Badge[]; // Top 3 selected by player
// Recent Activity
recentActivity: ActivityItem[];
}4. Rankings e Competição Saudável
4.1 Ranking Geral
interface GlobalRanking {
// Identificação
playerId: string;
rank: number;
previousRank: number;
rankChange: number;
// Métricas
rating: number;
ratingChange: number;
totalXP: number;
level: number;
// Estatísticas
matchesPlayed: number;
wins: number;
losses: number;
winRate: number;
// Contexto
percentile: number;
tier: 'bronze' | 'silver' | 'gold' | 'platinum' | 'diamond' | 'master';
// Temporal
updatedAt: Date;
}
class RankingService {
async getGlobalRanking(
limit: number = 100,
offset: number = 0
): Promise<Ranking[]> {
return await db.players
.orderBy('rating', 'desc')
.limit(limit)
.offset(offset)
.toArray();
}
async getPlayerRanking(playerId: string): Promise<PlayerRankingContext> {
const player = await this.getPlayer(playerId);
const rank = await this.calculateRank(player.rating);
// Jogadores próximos (contexto)
const above = await this.getPlayersAbove(player.rating, 5);
const below = await this.getPlayersBelow(player.rating, 5);
return {
player,
rank,
playersAbove: above,
playersBelow: below,
percentile: await this.calculatePercentile(rank)
};
}
}4.2 Rankings por Categoria
interface CategoryRanking {
type: RankingType;
filters: RankingFilters;
rankings: Ranking[];
}
interface RankingFilters {
sport?: string;
arena?: string;
ageGroup?: '18-25' | '26-35' | '36-45' | '46+';
gender?: 'male' | 'female' | 'other' | 'mixed';
levelRange?: { min: number; max: number };
location?: { city: string; state: string };
}
class CategoryRankingService {
// Ranking por esporte
async getSportRanking(sport: string): Promise<Ranking[]> {
return await db.rankings
.where('sport').equals(sport)
.sortBy('rating');
}
// Ranking por faixa de nível (fair play)
async getLevelRanking(level: number): Promise<Ranking[]> {
const range = this.getLevelRange(level);
return await db.rankings
.where('level')
.between(range.min, range.max)
.sortBy('rating');
}
private getLevelRange(level: number): { min: number; max: number } {
// Agrupa em faixas de 10 níveis
const tier = Math.floor(level / 10) * 10;
return {
min: tier,
max: tier + 9
};
}
// Ranking por idade (opcional, respeitando privacidade)
async getAgeGroupRanking(ageGroup: string): Promise<Ranking[]> {
return await db.rankings
.where('ageGroup').equals(ageGroup)
.sortBy('rating');
}
}4.3 Rankings Temporais
enum RankingPeriod {
DAILY = 'daily',
WEEKLY = 'weekly',
MONTHLY = 'monthly',
SEASONAL = 'seasonal',
ALLTIME = 'alltime'
}
interface TemporalRanking {
period: RankingPeriod;
startDate: Date;
endDate: Date;
rankings: Ranking[];
isActive: boolean;
}
class TemporalRankingService {
async getWeeklyRanking(): Promise<TemporalRanking> {
const now = new Date();
const startOfWeek = startOfWeek(now);
const endOfWeek = endOfWeek(now);
const rankings = await db.matches
.where('date')
.between(startOfWeek, endOfWeek)
.toArray();
const aggregated = this.aggregateByPlayer(rankings);
return {
period: RankingPeriod.WEEKLY,
startDate: startOfWeek,
endDate: endOfWeek,
rankings: this.sortByRating(aggregated),
isActive: true
};
}
async getMonthlyChampions(): Promise<MonthlyChampion[]> {
// Retorna campeões mensais históricos
return await db.monthlyRankings
.where('rank').equals(1)
.toArray();
}
// Reset semanal com recompensas
async resetWeeklyRanking(): Promise<void> {
const champions = await this.getTopPlayers(10);
// Distribuir recompensas
for (const [index, player] of champions.entries()) {
const reward = this.getWeeklyReward(index + 1);
await this.rewardPlayer(player.id, reward);
}
// Arquivar ranking atual
await this.archiveRanking(RankingPeriod.WEEKLY);
// Criar novo ranking
await this.createNewRanking(RankingPeriod.WEEKLY);
}
private getWeeklyReward(rank: number): Reward {
const rewards = {
1: { xp: 5000, arenaCoins: 1000, badge: 'weekly_champion' },
2: { xp: 3000, arenaCoins: 600 },
3: { xp: 2000, arenaCoins: 400 },
// 4-10
default: { xp: 1000, arenaCoins: 200 }
};
return rewards[rank] || rewards.default;
}
}4.4 Design Não-Punitivo
Princípios de Competição Saudável
interface HealthyCompetitionPrinciples {
// 1. Progressão sempre positiva
noNegativeXP: true;
noLevelDowngrade: true;
noRatingDecrease?: false; // Rating pode cair, mas não Level/XP
// 2. Comparação contextual
compareWithSimilarLevel: true;
showOnlyNearbyRanks: true;
hideGlobalIfTooFar: true;
// 3. Múltiplas formas de "vencer"
multipleTiers: true;
personalBests: true;
improvementHighlight: true;
// 4. Participação recompensada
participationXP: true;
effortRecognition: true;
consistencyBonus: true;
// 5. Sem shaming
noPublicFailures: true;
privateStats: true;
optInLeaderboards: true;
}
class HealthyCompetition {
// Mostrar ranking contextual
getContextualRanking(player: Player): ContextualRanking {
return {
// Mostrar apenas jogadores próximos
nearbyPlayers: this.getNearbyPlayers(player, 10),
// Comparar com média do nível
averageAtLevel: this.getAverageStats(player.level),
// Mostrar melhoria pessoal
personalProgress: {
lastWeek: player.stats.lastWeek,
thisWeek: player.stats.thisWeek,
improvement: this.calculateImprovement(player)
},
// Destacar vitórias pessoais
personalBests: [
{ metric: 'Win Streak', value: 12, isNew: false },
{ metric: 'Best Score', value: 45, isNew: true },
{ metric: 'Matches This Month', value: 28, isNew: true }
]
};
}
// Celebrar pequenas vitórias
generateEncouragement(player: Player, match: Match): string[] {
const messages = [];
if (!match.won) {
// Mesmo na derrota, encontrar pontos positivos
if (match.personalBest) {
messages.push("🎯 Seu melhor desempenho pessoal!");
}
if (match.improvement) {
messages.push("📈 Você está melhorando!");
}
messages.push("💪 +25 XP por esforço");
}
return messages;
}
}UI de Ranking Não-Punitiva
interface RankingUI {
// Mostrar contexto positivo
displayMode: 'nearby' | 'tier' | 'friends';
// Mensagens encorajadoras
encouragementMessages: {
rising: "Você subiu 5 posições esta semana! 📈",
stable: "Você mantém seu desempenho consistente 🎯",
falling: "Continue jogando! Você já jogou 3 partidas esta semana 💪"
};
// Highlight de conquistas
highlights: {
personalBest: boolean;
improvement: number;
consistency: boolean;
};
// Opt-in para ranking público
privacy: {
showInGlobalRanking: boolean;
showStats: boolean;
showToFriendsOnly: boolean;
};
}5. Conquistas e Badges
5.1 Badges de Participação
const participationBadges: BadgeDefinition[] = [
{
id: 'first_match',
name: 'Primeiro Passo',
description: 'Completou sua primeira partida',
icon: '🎯',
rarity: 'common',
xpReward: 50,
requirement: { matches: 1 }
},
{
id: 'regular_player',
name: 'Jogador Regular',
description: 'Completou 10 partidas',
icon: '⚽',
rarity: 'common',
xpReward: 200,
arenaCoinsReward: 50,
requirement: { matches: 10 }
},
{
id: 'dedicated_player',
name: 'Dedicado',
description: 'Completou 50 partidas',
icon: '🏆',
rarity: 'uncommon',
xpReward: 500,
arenaCoinsReward: 150,
requirement: { matches: 50 }
},
{
id: 'hardcore_player',
name: 'Hardcore',
description: 'Completou 100 partidas',
icon: '🔥',
rarity: 'rare',
xpReward: 1000,
arenaCoinsReward: 300,
requirement: { matches: 100 }
},
{
id: 'legend_player',
name: 'Lenda',
description: 'Completou 500 partidas',
icon: '👑',
rarity: 'epic',
xpReward: 5000,
arenaCoinsReward: 1000,
requirement: { matches: 500 }
},
{
id: 'weekend_warrior',
name: 'Guerreiro de Fim de Semana',
description: 'Jogou 20 partidas em um fim de semana',
icon: '🎮',
rarity: 'uncommon',
xpReward: 300,
arenaCoinsReward: 100,
requirement: { matches: 20, timeframe: 'weekend' }
},
{
id: 'night_owl',
name: 'Coruja Noturna',
description: 'Jogou 10 partidas após 22h',
icon: '🦉',
rarity: 'uncommon',
xpReward: 250,
requirement: { matches: 10, time: 'night' }
},
{
id: 'early_bird',
name: 'Madrugador',
description: 'Jogou 10 partidas antes das 8h',
icon: '🌅',
rarity: 'uncommon',
xpReward: 250,
requirement: { matches: 10, time: 'morning' }
}
];5.2 Badges de Performance
const performanceBadges: BadgeDefinition[] = [
// Futebol
{
id: 'hat_trick',
name: 'Hat Trick',
description: 'Marcou 3 gols em uma partida',
sport: 'football',
icon: '⚽⚽⚽',
rarity: 'rare',
xpReward: 500,
arenaCoinsReward: 150,
requirement: { goals: 3, inOneMatch: true }
},
{
id: 'clean_sheet',
name: 'Jogo Limpo',
description: 'Venceu sem sofrer gols (goleiro)',
sport: 'football',
icon: '🧤',
rarity: 'uncommon',
xpReward: 300,
requirement: { position: 'goalkeeper', goalsConceded: 0, won: true }
},
// Basquete
{
id: 'triple_double',
name: 'Triple Double',
description: '10+ pontos, rebotes e assistências',
sport: 'basketball',
icon: '🏀',
rarity: 'epic',
xpReward: 1000,
arenaCoinsReward: 300,
requirement: {
points: { min: 10 },
rebounds: { min: 10 },
assists: { min: 10 }
}
},
{
id: 'sharpshooter',
name: 'Atirador de Elite',
description: '80%+ de aproveitamento (mín. 10 arremessos)',
sport: 'basketball',
icon: '🎯',
rarity: 'rare',
xpReward: 500,
requirement: {
shootingPercentage: { min: 80 },
minimumShots: 10
}
},
// Universal
{
id: 'perfect_game',
name: 'Jogo Perfeito',
description: 'Venceu sem sofrer pontos',
icon: '💯',
rarity: 'epic',
xpReward: 1000,
arenaCoinsReward: 500,
requirement: { pointsConceded: 0, won: true }
},
{
id: 'comeback_king',
name: 'Rei da Virada',
description: 'Venceu após estar perdendo por 10+ pontos',
icon: '🔄',
rarity: 'rare',
xpReward: 750,
arenaCoinsReward: 200,
requirement: {
comeback: true,
deficitSize: { min: 10 }
}
},
{
id: 'domination',
name: 'Dominação',
description: 'Venceu por 20+ pontos de diferença',
icon: '💪',
rarity: 'uncommon',
xpReward: 400,
requirement: {
won: true,
marginOfVictory: { min: 20 }
}
},
{
id: 'clutch_player',
name: 'Clutch',
description: 'Venceu 5 partidas por diferença mínima (1-3 pontos)',
icon: '⏱️',
rarity: 'rare',
xpReward: 600,
requirement: {
wins: 5,
marginOfVictory: { max: 3 }
}
}
];5.3 Badges Sociais
const socialBadges: BadgeDefinition[] = [
{
id: 'team_player',
name: 'Espírito de Equipe',
description: 'Jogou 10 partidas com o mesmo time',
icon: '🤝',
rarity: 'uncommon',
xpReward: 300,
arenaCoinsReward: 100,
requirement: {
sameTeamMatches: 10
}
},
{
id: 'ambassador',
name: 'Embaixador',
description: 'Convidou 5 amigos que jogaram pelo menos 1 partida',
icon: '📢',
rarity: 'rare',
xpReward: 750,
arenaCoinsReward: 300,
requirement: {
invitedFriends: 5,
friendsPlayed: true
}
},
{
id: 'community_hero',
name: 'Herói da Comunidade',
description: 'Recebeu 50 curtidas em highlights',
icon: '❤️',
rarity: 'uncommon',
xpReward: 400,
requirement: {
likesReceived: 50
}
},
{
id: 'supporter',
name: 'Torcedor Fiel',
description: 'Fez check-in como torcedor em 50 partidas',
icon: '📣',
rarity: 'uncommon',
xpReward: 300,
requirement: {
spectatorCheckIns: 50
}
},
{
id: 'organizer',
name: 'Organizador',
description: 'Criou 10 partidas que aconteceram',
icon: '📅',
rarity: 'rare',
xpReward: 500,
arenaCoinsReward: 200,
requirement: {
matchesCreated: 10,
matchesCompleted: true
}
},
{
id: 'mentor',
name: 'Mentor',
description: 'Jogou 20 partidas ajudando jogadores nível 1-5',
icon: '🎓',
rarity: 'rare',
xpReward: 600,
requirement: {
matchesWithNewbies: 20,
teammateLevel: { max: 5 }
}
},
{
id: 'social_butterfly',
name: 'Borboleta Social',
description: 'Jogou com 50 jogadores diferentes',
icon: '🦋',
rarity: 'uncommon',
xpReward: 400,
requirement: {
uniqueTeammates: 50
}
}
];5.4 Badges de Evolução
const evolutionBadges: BadgeDefinition[] = [
{
id: 'rising_star',
name: 'Estrela em Ascensão',
description: 'Alcançou nível 10',
icon: '⭐',
rarity: 'common',
xpReward: 500,
arenaCoinsReward: 100,
requirement: { level: 10 }
},
{
id: 'veteran',
name: 'Veterano',
description: 'Alcançou nível 25',
icon: '🎖️',
rarity: 'uncommon',
xpReward: 1000,
arenaCoinsReward: 300,
requirement: { level: 25 }
},
{
id: 'master',
name: 'Mestre',
description: 'Alcançou nível 50',
icon: '🥇',
rarity: 'rare',
xpReward: 2500,
arenaCoinsReward: 750,
requirement: { level: 50 }
},
{
id: 'first_win',
name: 'Primeira Vitória',
description: 'Conquistou sua primeira vitória',
icon: '🏆',
rarity: 'common',
xpReward: 100,
arenaCoinsReward: 25,
requirement: { wins: 1 }
},
{
id: 'improvement',
name: 'Em Evolução',
description: 'Melhorou win rate em 50% nos últimos 30 dias',
icon: '📈',
rarity: 'rare',
xpReward: 750,
arenaCoinsReward: 200,
requirement: {
winRateImprovement: { min: 50 },
timeframe: '30days'
}
},
{
id: 'skill_master',
name: 'Mestre de Habilidade',
description: 'Maximizou uma habilidade (nível 10)',
icon: '🎯',
rarity: 'rare',
xpReward: 1000,
requirement: {
skillMaxed: true
}
},
{
id: 'all_rounder',
name: 'Completo',
description: 'Alcançou nível 5+ em todas as habilidades',
icon: '🌟',
rarity: 'epic',
xpReward: 2000,
arenaCoinsReward: 500,
requirement: {
allSkillsMinLevel: 5
}
},
{
id: 'multi_sport',
name: 'Multi-Esportivo',
description: 'Jogou 20+ partidas em 3 esportes diferentes',
icon: '🎾⚽🏀',
rarity: 'uncommon',
xpReward: 600,
requirement: {
sports: 3,
matchesPerSport: { min: 20 }
}
}
];5.5 Badges Raros/Épicos
const rareBadges: BadgeDefinition[] = [
{
id: 'unicorn',
name: 'Unicórnio',
description: 'Venceu com score exato de 111 pontos',
icon: '🦄',
rarity: 'legendary',
xpReward: 5000,
arenaCoinsReward: 2000,
requirement: {
finalScore: 111,
won: true
},
firstEarnedBy: null,
earnedByCount: 0
},
{
id: 'perfect_week',
name: 'Semana Perfeita',
description: 'Venceu 7 partidas em 7 dias consecutivos',
icon: '💎',
rarity: 'legendary',
xpReward: 3000,
arenaCoinsReward: 1500,
requirement: {
wins: 7,
consecutive: true,
timeframe: '7days'
}
},
{
id: 'grand_slam',
name: 'Grand Slam',
description: 'Venceu torneio com 16+ jogadores',
icon: '🏆',
rarity: 'epic',
xpReward: 2500,
arenaCoinsReward: 1000,
requirement: {
tournamentWin: true,
tournamentSize: { min: 16 }
}
},
{
id: 'hall_of_fame',
name: 'Hall da Fama',
description: 'Alcançou Top 10 do ranking global',
icon: '🌟',
rarity: 'epic',
xpReward: 5000,
arenaCoinsReward: 2000,
requirement: {
globalRank: { max: 10 }
}
},
{
id: 'goat',
name: 'GOAT',
description: 'Manteve #1 no ranking global por 30 dias',
icon: '🐐',
rarity: 'legendary',
xpReward: 10000,
arenaCoinsReward: 5000,
requirement: {
globalRank: 1,
duration: '30days'
},
firstEarnedBy: null,
earnedByCount: 0
},
{
id: 'iron_man',
name: 'Homem de Ferro',
description: 'Jogou 50 partidas em 7 dias',
icon: '⚙️',
rarity: 'epic',
xpReward: 2000,
arenaCoinsReward: 750,
requirement: {
matches: 50,
timeframe: '7days'
}
},
{
id: 'centurion',
name: 'Centurião',
description: 'Alcançou streak de 100 dias',
icon: '💯',
rarity: 'legendary',
xpReward: 10000,
arenaCoinsReward: 5000,
requirement: {
loginStreak: 100
}
},
{
id: 'underdog',
name: 'Azarão Vencedor',
description: 'Venceu 10 partidas sendo underdog (rating 200+ menor)',
icon: '🐕',
rarity: 'epic',
xpReward: 1500,
arenaCoinsReward: 600,
requirement: {
underdogWins: 10,
ratingDifference: { min: 200 }
}
}
];6. Partidas "Valendo"
6.1 Conceito
Partidas "valendo" permitem que jogadores apostem ArenaCoins em partidas, aumentando emoção e stakes sem envolver dinheiro real (cumprindo regulamentações).
6.2 Tipos de Partidas
enum MatchType {
CASUAL = 'casual', // Sem stakes
FRIENDLY = 'friendly', // Stakes mínimos (10-50 coins)
COMPETITIVE = 'competitive', // Stakes médios (50-200 coins)
HIGH_STAKES = 'high_stakes', // Stakes altos (200-1000 coins)
TOURNAMENT = 'tournament' // Definido pelo TO
}
interface StakedMatch {
id: string;
type: MatchType;
sport: string;
// Stakes
entryFee: number; // ArenaCoins por jogador
prizePool: number; // Total apostado
distribution: PrizeDistribution;
// Participantes
minPlayers: number;
maxPlayers: number;
currentPlayers: Player[];
// Requisitos
minLevel: number;
minRating?: number;
// Status
status: 'open' | 'full' | 'in_progress' | 'completed';
startsAt: Date;
// Regras
rules: MatchRules;
}
interface PrizeDistribution {
winner: number; // % para vencedor
runnerUp?: number; // % para 2º lugar (se aplicável)
participation: number; // % devolvido para todos
platformFee: number; // Taxa da plataforma (5%)
}
const distributionTemplates = {
winner_takes_all: {
winner: 95,
participation: 0,
platformFee: 5
},
balanced: {
winner: 70,
runnerUp: 20,
participation: 5,
platformFee: 5
},
participation_reward: {
winner: 60,
runnerUp: 15,
participation: 20,
platformFee: 5
}
};6.3 Fluxo de Partida Valendo
class StakedMatchService {
// 1. Criação de partida
async createStakedMatch(
creator: Player,
config: StakedMatchConfig
): Promise<StakedMatch> {
// Validações
if (creator.level < 15) {
throw new Error('Level mínimo 15 para criar partidas valendo');
}
if (creator.arenaCoins < config.entryFee) {
throw new Error('ArenaCoins insuficientes');
}
// Congelar entry fee do criador
await this.walletService.freeze(creator.id, config.entryFee);
return await this.matchRepository.create({
...config,
creatorId: creator.id,
status: 'open',
currentPlayers: [creator]
});
}
// 2. Join em partida
async joinStakedMatch(
player: Player,
matchId: string
): Promise<void> {
const match = await this.getMatch(matchId);
// Validações
this.validateJoin(player, match);
// Congelar entry fee
await this.walletService.freeze(player.id, match.entryFee);
// Adicionar jogador
match.currentPlayers.push(player);
// Se lotou, iniciar
if (match.currentPlayers.length === match.maxPlayers) {
await this.startMatch(match);
}
await this.matchRepository.update(match);
}
// 3. Finalização e distribuição
async finalizeStakedMatch(
matchId: string,
results: MatchResults
): Promise<void> {
const match = await this.getMatch(matchId);
// Calcular prêmios
const prizes = this.calculatePrizes(match, results);
// Distribuir
for (const prize of prizes) {
await this.walletService.unfreeze(prize.playerId, match.entryFee);
await this.walletService.add(prize.playerId, prize.amount, {
type: 'match_prize',
matchId: match.id,
description: `Prêmio - ${match.type}`
});
}
// Taxa da plataforma
const platformFee = match.prizePool * (match.distribution.platformFee / 100);
await this.walletService.collectFee(platformFee);
// Atualizar match
match.status = 'completed';
match.results = results;
await this.matchRepository.update(match);
// Notificar jogadores
await this.notifyPrizes(prizes);
}
private calculatePrizes(
match: StakedMatch,
results: MatchResults
): Prize[] {
const prizes: Prize[] = [];
const pool = match.prizePool;
const dist = match.distribution;
// Vencedor
const winner = results.winner;
prizes.push({
playerId: winner.id,
amount: pool * (dist.winner / 100),
position: 1
});
// 2º lugar (se aplicável)
if (dist.runnerUp && results.runnerUp) {
prizes.push({
playerId: results.runnerUp.id,
amount: pool * (dist.runnerUp / 100),
position: 2
});
}
// Prêmio de participação
if (dist.participation > 0) {
const participationAmount = (pool * (dist.participation / 100)) / match.currentPlayers.length;
for (const player of match.currentPlayers) {
prizes.push({
playerId: player.id,
amount: participationAmount,
position: null,
type: 'participation'
});
}
}
return prizes;
}
}6.4 Proteções e Fairness
interface StakedMatchProtections {
// Anti-fraude
minimumLevel: 15;
minimumMatchHistory: 20;
accountAge: '30days';
// Matchmaking justo
maxRatingDifference: 300;
similarLevelOnly: true;
// Proteções financeiras
dailyLossLimit: 1000; // Máximo de perda por dia
maxSingleStake: 1000; // Máximo por partida
cooldownAfterLoss: '1hour'; // Cooldown após perder
// Transparência
showPlayerStats: true;
showWinRate: true;
showHistory: true;
// Reversão
disputeWindow: '24hours';
refundOnDispute: 'automatic';
}
class FairPlayService {
validateStakedMatch(player: Player, match: StakedMatch): void {
// Level mínimo
if (player.level < match.minLevel) {
throw new Error(`Level mínimo: ${match.minLevel}`);
}
// Rating similar
if (match.minRating) {
const avgRating = this.getAverageRating(match.currentPlayers);
if (Math.abs(player.rating - avgRating) > 300) {
throw new Error('Rating muito diferente dos outros jogadores');
}
}
// Limite diário
const todayLosses = await this.getTodayLosses(player.id);
if (todayLosses >= 1000) {
throw new Error('Limite diário de perdas atingido. Tente amanhã.');
}
// Cooldown
const lastLoss = await this.getLastLoss(player.id);
if (lastLoss && this.isInCooldown(lastLoss)) {
throw new Error('Aguarde 1 hora após uma derrota antes de apostar novamente');
}
// Histórico mínimo
if (player.matchesPlayed < 20) {
throw new Error('Mínimo de 20 partidas para participar de partidas valendo');
}
}
// Sistema de disputa
async handleDispute(matchId: string, playerId: string, reason: string): Promise<void> {
const match = await this.getMatch(matchId);
// Congelar distribuição de prêmios
await this.freezePrizes(match);
// Criar ticket de disputa
const dispute = await this.disputeRepository.create({
matchId,
playerId,
reason,
status: 'under_review',
createdAt: new Date()
});
// Notificar moderadores
await this.notifyModerators(dispute);
// Auto-resolve casos óbvios
if (await this.canAutoResolve(dispute)) {
await this.resolveDispute(dispute.id, 'approved');
}
}
}6.5 UI/UX de Partidas Valendo
interface StakedMatchUI {
// Destacar stakes
visual: {
badge: '💰 VALENDO',
color: 'gold',
animation: 'shimmer'
};
// Info clara
display: {
entryFee: '50 ArenaCoins',
prizePool: '450 ArenaCoins',
yourPotentialWin: '427 ArenaCoins',
platformFee: '23 ArenaCoins (5%)'
};
// Confirmação
confirmation: {
title: 'Confirmar Aposta',
message: 'Você está apostando 50 ArenaCoins nesta partida.',
warnings: [
'Você pode perder seus ArenaCoins se não vencer',
'Certifique-se de que está pronto para jogar'
],
requireExplicitConfirm: true
};
// Transparência
playerInfo: {
showLevel: true,
showRating: true,
showWinRate: true,
showRecentMatches: true
};
}7. Integração com Wallet
7.1 ArenaCoins - A Moeda da Plataforma
interface ArenaCoin {
// Definição
name: 'ArenaCoin';
symbol: 'AC';
type: 'virtual_currency'; // Não é criptomoeda
decimal: 2;
// Propriedades
isRefundable: false; // Não pode ser sacado como dinheiro
isTransferable: true; // Pode ser transferido entre usuários
expiresAt: null; // Não expira
// Compliance
notACryptocurrency: true; // Importante legalmente
notRedeemableForCash: true;
virtualGoodOnly: true;
}
interface Wallet {
userId: string;
// Balanços
balance: number; // ArenaCoins disponíveis
frozen: number; // Congelados (em partidas, etc)
lifetime: number; // Total ganho na vida
// Histórico
transactions: Transaction[];
// Limites
dailySpendLimit: number;
dailyEarnLimit: number;
// Metadata
createdAt: Date;
updatedAt: Date;
}
interface Transaction {
id: string;
walletId: string;
type: TransactionType;
amount: number;
balance: number; // Saldo após transação
description: string;
metadata: object;
createdAt: Date;
}
enum TransactionType {
// Ganhos
MATCH_WIN = 'match_win',
CHALLENGE_COMPLETE = 'challenge_complete',
BADGE_UNLOCK = 'badge_unlock',
LEVEL_UP = 'level_up',
DAILY_REWARD = 'daily_reward',
REFERRAL_BONUS = 'referral_bonus',
PURCHASE = 'purchase',
// Gastos
MATCH_ENTRY = 'match_entry',
ITEM_PURCHASE = 'item_purchase',
TRANSFER_SENT = 'transfer_sent',
// Recebimentos
MATCH_PRIZE = 'match_prize',
TRANSFER_RECEIVED = 'transfer_received',
REFUND = 'refund',
// Sistema
ADMIN_ADJUSTMENT = 'admin_adjustment',
PROMOTION = 'promotion'
}7.2 Fontes de ArenaCoins
const arenaCoinsRewards = {
// Progressão
levelUp: {
1: 10,
5: 50,
10: 100,
15: 150,
20: 250,
25: 350,
30: 500,
// ...
50: 1000
},
// Atividades
checkIn: 5,
matchComplete: 10,
matchWin: 25,
matchMVP: 50,
// Desafios
dailyChallenge: 50,
weeklyChallenge: 200,
specialEvent: 500,
// Social
friendInvite: 150,
friendFirstMatch: 100,
// Badges
badgeCommon: 50,
badgeUncommon: 100,
badgeRare: 250,
badgeEpic: 500,
badgeLegendary: 1000,
// Streaks
streak3Days: 50,
streak7Days: 150,
streak30Days: 1000,
// Rankings
weeklyTop10: [1000, 600, 400, 300, 250, 200, 150, 125, 100, 100],
monthlyTop10: [5000, 3000, 2000, 1500, 1000, 750, 500, 400, 300, 200],
// Daily Login
day1: 10,
day2: 15,
day3: 20,
day4: 25,
day5: 30,
day6: 40,
day7: 100, // Bonus no 7º dia
// Compras (opcional, com dinheiro real)
iap: {
starter: { price: 4.99, coins: 100 },
basic: { price: 9.99, coins: 220 },
pro: { price: 19.99, coins: 500 },
elite: { price: 49.99, coins: 1500 }
}
};7.3 Usos de ArenaCoins
const arenaCoinsUses = {
// Partidas
stakedMatchEntry: {
friendly: 10-50,
competitive: 50-200,
highStakes: 200-1000
},
// Itens Cosméticos
avatars: 100-500,
frames: 200-1000,
badges: 300-1500,
themes: 500-2000,
emotes: 50-300,
celebrations: 200-800,
// Boost temporários (não pay-to-win)
xpBoost24h: 500, // +50% XP por 24h
xpBoost7d: 2000, // +50% XP por 7 dias
highlightSlots: 300, // +5 slots de highlights
// Social
giftToFriend: 50-1000,
highlightPromotion: 100, // Destacar highlight no feed
// Torneios
tournamentEntry: 100-500,
privateTournament: 1000, // Criar torneio privado
// Funcionalidades Premium
customBadge: 5000, // Badge customizado
nameChange: 500,
streakFreeze: 200, // Congelar streak por 1 dia
statsReset: 1000 // Reset de estatísticas (1x)
};7.4 Wallet Service
class WalletService {
// Adicionar ArenaCoins
async add(
userId: string,
amount: number,
transaction: Partial<Transaction>
): Promise<Wallet> {
const wallet = await this.getWallet(userId);
// Validar
if (amount <= 0) {
throw new Error('Amount must be positive');
}
// Atualizar saldo
wallet.balance += amount;
wallet.lifetime += amount;
// Registrar transação
await this.transactionRepository.create({
walletId: wallet.id,
amount,
balance: wallet.balance,
...transaction,
createdAt: new Date()
});
await this.walletRepository.update(wallet);
// Eventos
await this.eventBus.publish('wallet.balance_updated', {
userId,
newBalance: wallet.balance,
change: amount
});
return wallet;
}
// Remover ArenaCoins
async subtract(
userId: string,
amount: number,
transaction: Partial<Transaction>
): Promise<Wallet> {
const wallet = await this.getWallet(userId);
// Validar saldo
if (wallet.balance < amount) {
throw new Error('Insufficient balance');
}
wallet.balance -= amount;
await this.transactionRepository.create({
walletId: wallet.id,
amount: -amount,
balance: wallet.balance,
...transaction,
createdAt: new Date()
});
await this.walletRepository.update(wallet);
return wallet;
}
// Congelar (para partidas valendo)
async freeze(userId: string, amount: number): Promise<void> {
const wallet = await this.getWallet(userId);
if (wallet.balance < amount) {
throw new Error('Insufficient balance');
}
wallet.balance -= amount;
wallet.frozen += amount;
await this.walletRepository.update(wallet);
}
// Descongelar
async unfreeze(userId: string, amount: number): Promise<void> {
const wallet = await this.getWallet(userId);
wallet.frozen -= amount;
wallet.balance += amount;
await this.walletRepository.update(wallet);
}
// Transferir entre usuários
async transfer(
fromUserId: string,
toUserId: string,
amount: number,
message?: string
): Promise<void> {
// Validações
if (fromUserId === toUserId) {
throw new Error('Cannot transfer to yourself');
}
const fromWallet = await this.getWallet(fromUserId);
if (fromWallet.balance < amount) {
throw new Error('Insufficient balance');
}
// Taxa de transferência (5%)
const fee = amount * 0.05;
const netAmount = amount - fee;
// Debitar
await this.subtract(fromUserId, amount, {
type: TransactionType.TRANSFER_SENT,
description: `Transfer to ${toUserId}`,
metadata: { toUserId, message }
});
// Creditar
await this.add(toUserId, netAmount, {
type: TransactionType.TRANSFER_RECEIVED,
description: `Transfer from ${fromUserId}`,
metadata: { fromUserId, message }
});
// Taxa para plataforma
await this.collectFee(fee);
}
// Histórico
async getTransactions(
userId: string,
filters?: TransactionFilters
): Promise<Transaction[]> {
const wallet = await this.getWallet(userId);
let query = this.transactionRepository
.where('walletId').equals(wallet.id);
if (filters?.type) {
query = query.where('type').equals(filters.type);
}
if (filters?.startDate) {
query = query.where('createdAt').gte(filters.startDate);
}
return await query
.orderBy('createdAt', 'desc')
.limit(filters?.limit || 50)
.toArray();
}
}7.5 Cashback Gamificado
interface CashbackProgram {
// Níveis de cashback baseados em atividade
tiers: {
bronze: { minLevel: 1, cashback: 2 }, // 2% em ArenaCoins
silver: { minLevel: 10, cashback: 3 }, // 3%
gold: { minLevel: 25, cashback: 5 }, // 5%
platinum: { minLevel: 50, cashback: 7 } // 7%
};
// Aplicável em
applies: [
'item_purchase',
'staked_match_entry',
'tournament_entry'
];
// Exemplo
example: {
purchase: 1000, // ArenaCoins gastos
tier: 'gold',
cashback: 50 // 5% de 1000
};
}
class CashbackService {
async applyCashback(
userId: string,
amount: number,
type: TransactionType
): Promise<number> {
const player = await this.playerService.getPlayer(userId);
const tier = this.getCashbackTier(player.level);
if (!this.isEligible(type)) {
return 0;
}
const cashback = amount * (tier.cashback / 100);
await this.walletService.add(userId, cashback, {
type: TransactionType.CASHBACK,
description: `Cashback ${tier.cashback}% (tier: ${tier.name})`,
metadata: { originalAmount: amount, originalType: type }
});
return cashback;
}
}8. Campeonatos e Torneios
8.1 Tipos de Torneios
enum TournamentType {
SINGLE_ELIMINATION = 'single_elimination', // Mata-mata
DOUBLE_ELIMINATION = 'double_elimination', // Com repescagem
ROUND_ROBIN = 'round_robin', // Todos contra todos
SWISS = 'swiss', // Sistema suíço
LEAGUE = 'league' // Liga com pontos corridos
}
enum TournamentAccess {
PUBLIC = 'public', // Qualquer um pode entrar
INVITE_ONLY = 'invite_only', // Apenas convidados
LEVEL_GATED = 'level_gated', // Requer nível mínimo
PAID = 'paid' // Entry fee em ArenaCoins
}
interface Tournament {
id: string;
name: string;
description: string;
sport: string;
// Tipo e formato
type: TournamentType;
access: TournamentAccess;
// Participantes
minParticipants: number;
maxParticipants: number;
currentParticipants: Player[];
// Entry
entryFee: number;
minLevel: number;
minRating?: number;
// Premiação
prizePool: number;
prizeDistribution: TournamentPrizes;
// Datas
registrationStart: Date;
registrationEnd: Date;
startDate: Date;
endDate?: Date;
// Status
status: 'registration' | 'ready' | 'in_progress' | 'completed' | 'cancelled';
// Brackets/Grupos
brackets?: Bracket[];
groups?: Group[];
standings?: Standing[];
// Organizador
organizerId: string;
organizer: Player;
// Metadata
rules: string;
streamUrl?: string;
createdAt: Date;
updatedAt: Date;
}
interface Bracket {
round: number;
matches: BracketMatch[];
}
interface BracketMatch {
id: string;
player1: Player;
player2: Player;
winner?: Player;
score?: { player1: number; player2: number };
scheduledAt?: Date;
completedAt?: Date;
status: 'pending' | 'in_progress' | 'completed';
}
interface TournamentPrizes {
first: number;
second: number;
third: number;
fourth?: number;
participation?: number;
}8.2 Gerenciamento de Torneios
class TournamentService {
// Criar torneio
async createTournament(
organizer: Player,
config: TournamentConfig
): Promise<Tournament> {
// Validar permissões (requer nível 30)
if (organizer.level < 30) {
throw new Error('Level mínimo 30 para criar torneios');
}
// Validar prize pool
if (config.prizePool < config.entryFee * config.minParticipants) {
throw new Error('Prize pool insuficiente');
}
const tournament = await this.tournamentRepository.create({
...config,
organizerId: organizer.id,
status: 'registration',
currentParticipants: [],
createdAt: new Date()
});
// Notificar comunidade
await this.notificationService.broadcastTournament(tournament);
return tournament;
}
// Registrar participante
async registerParticipant(
player: Player,
tournamentId: string
): Promise<void> {
const tournament = await this.getTournament(tournamentId);
// Validações
this.validateRegistration(player, tournament);
// Cobrar entry fee
if (tournament.entryFee > 0) {
await this.walletService.subtract(player.id, tournament.entryFee, {
type: TransactionType.TOURNAMENT_ENTRY,
description: `Entry: ${tournament.name}`,
metadata: { tournamentId }
});
tournament.prizePool += tournament.entryFee * 0.95; // 5% taxa
}
// Adicionar participante
tournament.currentParticipants.push(player);
await this.tournamentRepository.update(tournament);
// Notificar
await this.notificationService.notifyTournamentRegistration(
player,
tournament
);
}
// Gerar brackets (single elimination)
async generateBrackets(tournamentId: string): Promise<Bracket[]> {
const tournament = await this.getTournament(tournamentId);
if (tournament.status !== 'ready') {
throw new Error('Tournament not ready');
}
const participants = tournament.currentParticipants;
const rounds = Math.ceil(Math.log2(participants.length));
// Shuffle e seed players
const seeded = this.seedPlayers(participants);
const brackets: Bracket[] = [];
// Round 1
const round1Matches: BracketMatch[] = [];
for (let i = 0; i < seeded.length; i += 2) {
round1Matches.push({
id: `r1m${i/2}`,
player1: seeded[i],
player2: seeded[i + 1],
status: 'pending'
});
}
brackets.push({ round: 1, matches: round1Matches });
// Rounds subsequentes (placeholders)
for (let r = 2; r <= rounds; r++) {
const matches: BracketMatch[] = [];
const numMatches = Math.pow(2, rounds - r);
for (let m = 0; m < numMatches; m++) {
matches.push({
id: `r${r}m${m}`,
player1: null!, // TBD
player2: null!, // TBD
status: 'pending'
});
}
brackets.push({ round: r, matches });
}
tournament.brackets = brackets;
tournament.status = 'in_progress';
await this.tournamentRepository.update(tournament);
return brackets;
}
// Registrar resultado de match
async reportMatchResult(
tournamentId: string,
matchId: string,
winnerId: string,
score: { player1: number; player2: number }
): Promise<void> {
const tournament = await this.getTournament(tournamentId);
const match = this.findMatch(tournament.brackets!, matchId);
if (!match) {
throw new Error('Match not found');
}
// Atualizar match
match.winner = match.player1.id === winnerId ? match.player1 : match.player2;
match.score = score;
match.completedAt = new Date();
match.status = 'completed';
// Avançar vencedor para próximo round
await this.advanceWinner(tournament, match);
await this.tournamentRepository.update(tournament);
// Verificar se torneio acabou
if (this.isTournamentComplete(tournament)) {
await this.finalizeTournament(tournament);
}
}
// Finalizar torneio e distribuir prêmios
private async finalizeTournament(tournament: Tournament): Promise<void> {
const finalMatch = this.getFinalMatch(tournament);
const champion = finalMatch.winner!;
const runnerUp = finalMatch.player1.id === champion.id
? finalMatch.player2
: finalMatch.player1;
// Distribuir prêmios
const prizes = tournament.prizeDistribution;
await this.awardPrize(champion.id, prizes.first, tournament, 1);
await this.awardPrize(runnerUp.id, prizes.second, tournament, 2);
// 3º e 4º lugar (se houver)
if (prizes.third) {
const thirdPlace = this.getThirdPlace(tournament);
await this.awardPrize(thirdPlace.id, prizes.third, tournament, 3);
}
// Badge de campeão
await this.badgeService.award(champion.id, {
id: `tournament_winner_${tournament.id}`,
name: `Campeão: ${tournament.name}`,
rarity: 'epic'
});
tournament.status = 'completed';
tournament.endDate = new Date();
await this.tournamentRepository.update(tournament);
// Anunciar campeão
await this.notificationService.announceTournamentWinner(
tournament,
champion
);
}
private async awardPrize(
playerId: string,
amount: number,
tournament: Tournament,
position: number
): Promise<void> {
await this.walletService.add(playerId, amount, {
type: TransactionType.TOURNAMENT_PRIZE,
description: `${position}º lugar - ${tournament.name}`,
metadata: {
tournamentId: tournament.id,
position
}
});
}
}8.3 Formatos Especiais
Round Robin (Todos contra todos)
class RoundRobinTournament {
generateFixtures(participants: Player[]): Fixture[] {
const fixtures: Fixture[] = [];
const n = participants.length;
// Algoritmo round-robin circular
for (let round = 0; round < n - 1; round++) {
for (let i = 0; i < n / 2; i++) {
const player1 = participants[i];
const player2 = participants[n - 1 - i];
fixtures.push({
round: round + 1,
player1,
player2,
scheduledDate: this.calculateMatchDate(round, i)
});
}
// Rotacionar jogadores (exceto o primeiro)
participants = [
participants[0],
participants[n - 1],
...participants.slice(1, n - 1)
];
}
return fixtures;
}
calculateStandings(fixtures: Fixture[]): Standing[] {
const standings = new Map<string, Standing>();
for (const fixture of fixtures) {
if (!fixture.completed) continue;
// Atualizar estatísticas
this.updateStanding(standings, fixture.player1, fixture);
this.updateStanding(standings, fixture.player2, fixture);
}
return Array.from(standings.values())
.sort((a, b) => {
// Ordenar por: pontos, saldo, gols
if (b.points !== a.points) return b.points - a.points;
if (b.goalDifference !== a.goalDifference) return b.goalDifference - a.goalDifference;
return b.goalsFor - a.goalsFor;
});
}
}9. Vídeo e Highlights
9.1 Sistema de Gravação
interface MatchRecording {
matchId: string;
videoUrl: string;
duration: number;
format: 'mp4' | 'webm';
resolution: '720p' | '1080p' | '4k';
// Metadata
sport: string;
players: Player[];
finalScore: Score;
recordedAt: Date;
// Highlights marcados
highlights: Highlight[];
// Processamento
status: 'processing' | 'ready' | 'failed';
thumbnailUrl: string;
}
interface Highlight {
id: string;
matchId: string;
playerId: string;
// Timing
startTime: number; // Segundos
endTime: number;
duration: number;
// Tipo
type: HighlightType;
description: string;
// Clipe gerado
clipUrl: string;
thumbnailUrl: string;
// Social
likes: number;
views: number;
shares: number;
comments: Comment[];
// Tags
tags: string[];
createdAt: Date;
}
enum HighlightType {
GOAL = 'goal',
ASSIST = 'assist',
SAVE = 'save',
DUNK = 'dunk',
THREE_POINTER = 'three_pointer',
BLOCK = 'block',
STEAL = 'steal',
CUSTOM = 'custom'
}9.2 Botão de Highlight Durante Partida
interface HighlightButton {
// Estado
isRecording: boolean;
isProcessing: boolean;
// Configurações
captureWindow: 15; // Captura 15s antes + 5s depois do clique
// Buffer
replayBuffer: CircularBuffer<VideoFrame>;
bufferSize: 20; // 20 segundos em buffer
}
class HighlightService {
// Quando jogador clica no botão
async captureHighlight(
matchId: string,
playerId: string,
type: HighlightType,
timestamp: number
): Promise<Highlight> {
// Marcar momento
const startTime = Math.max(0, timestamp - 15); // 15s antes
const endTime = timestamp + 5; // 5s depois
const highlight = await this.highlightRepository.create({
matchId,
playerId,
type,
startTime,
endTime,
duration: 20,
status: 'processing'
});
// Processar assíncrono
this.processHighlight(highlight);
return highlight;
}
// Processamento de vídeo
private async processHighlight(highlight: Highlight): Promise<void> {
const match = await this.matchService.getMatch(highlight.matchId);
// Extrair clipe do vídeo completo
const clip = await this.videoService.extractClip(
match.videoUrl,
highlight.startTime,
highlight.endTime
);
// Aplicar efeitos
const enhanced = await this.videoService.enhance(clip, {
slowMotion: true,
replay: true,
overlays: {
playerName: true,
timestamp: true,
score: true
}
});
// Upload
const clipUrl = await this.storageService.upload(enhanced);
// Gerar thumbnail
const thumbnail = await this.videoService.generateThumbnail(enhanced);
const thumbnailUrl = await this.storageService.upload(thumbnail);
// Atualizar highlight
highlight.clipUrl = clipUrl;
highlight.thumbnailUrl = thumbnailUrl;
highlight.status = 'ready';
await this.highlightRepository.update(highlight);
// Notificar player
await this.notificationService.notifyHighlightReady(
highlight.playerId,
highlight
);
}
}9.3 Melhores Momentos Automáticos
interface AutoHighlightDetection {
// ML Model para detectar momentos importantes
model: 'highlight_detector_v1';
// Eventos que triggam detecção automática
triggers: [
'goal_scored',
'assist_made',
'save_made',
'three_pointer',
'dunk',
'comeback_moment',
'game_winning_play'
];
// Confidence threshold
minConfidence: 0.85;
}
class AutoHighlightService {
async detectHighlights(matchId: string): Promise<Highlight[]> {
const match = await this.matchService.getMatch(matchId);
const events = await this.eventService.getMatchEvents(matchId);
const highlights: Highlight[] = [];
for (const event of events) {
// Verificar se é highlight-worthy
if (this.isHighlightEvent(event)) {
const highlight = await this.createAutoHighlight(match, event);
highlights.push(highlight);
}
}
// Processar todos
await Promise.all(
highlights.map(h => this.highlightService.processHighlight(h))
);
return highlights;
}
private isHighlightEvent(event: MatchEvent): boolean {
const highlightEvents = [
'goal',
'assist',
'save',
'three_pointer',
'dunk',
'block',
'steal',
'game_winner'
];
return highlightEvents.includes(event.type);
}
// Detectar momentos "clutch"
async detectClutchMoments(matchId: string): Promise<Highlight[]> {
const match = await this.matchService.getMatch(matchId);
const clutchMoments: Highlight[] = [];
// Último minuto com diferença <= 5 pontos
const finalMinuteEvents = match.events.filter(e =>
e.timestamp >= match.duration - 60 &&
Math.abs(e.scoreAfter.team1 - e.scoreAfter.team2) <= 5
);
for (const event of finalMinuteEvents) {
if (['goal', 'basket', 'score'].includes(event.type)) {
clutchMoments.push(await this.createClutchHighlight(match, event));
}
}
return clutchMoments;
}
}9.4 Compartilhamento Social
interface SocialSharing {
platforms: ['twitter', 'instagram', 'tiktok', 'whatsapp', 'facebook'];
// Formatação por plataforma
formats: {
twitter: {
maxDuration: 140,
aspectRatio: '16:9',
maxSize: '512MB'
},
instagram: {
maxDuration: 60,
aspectRatio: '9:16', // Stories
maxSize: '100MB'
},
tiktok: {
maxDuration: 60,
aspectRatio: '9:16',
maxSize: '287MB'
}
};
// Templates de caption
captions: {
goal: "🔥 Golaço! {player} na partida de {sport}",
assist: "👌 Assistência perfeita de {player}!",
win: "🏆 Vitória de {player}! {score}",
highlight: "⚡ Momento épico de {player}!"
};
}
class SocialSharingService {
async shareHighlight(
highlightId: string,
platform: string,
customCaption?: string
): Promise<ShareResult> {
const highlight = await this.highlightService.getHighlight(highlightId);
// Adaptar vídeo para plataforma
const adapted = await this.adaptForPlatform(highlight, platform);
// Gerar caption
const caption = customCaption || this.generateCaption(highlight);
// Share URL
const shareUrl = this.generateShareUrl(platform, adapted, caption);
// Registrar share
await this.analyticsService.trackShare(highlightId, platform);
// Incrementar contador
highlight.shares++;
await this.highlightRepository.update(highlight);
return {
success: true,
url: shareUrl,
platform
};
}
// Deep links
generateShareUrl(platform: string, video: Video, caption: string): string {
const encodedCaption = encodeURIComponent(caption);
const videoUrl = encodeURIComponent(video.url);
const deepLinks = {
twitter: `https://twitter.com/intent/tweet?text=${encodedCaption}&url=${videoUrl}`,
instagram: `instagram://library?video=${videoUrl}`,
tiktok: `snssdk1180://uploadvideo?video=${videoUrl}`,
whatsapp: `whatsapp://send?text=${encodedCaption}%20${videoUrl}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${videoUrl}"e=${encodedCaption}`
};
return deepLinks[platform] || videoUrl;
}
}9.5 Integração com Conquistas
interface HighlightBadges {
viral: {
name: 'Viral',
description: 'Highlight com 1000+ views',
requirement: { views: 1000 }
},
superstar: {
name: 'Superstar',
description: 'Highlight com 10,000+ views',
requirement: { views: 10000 }
},
creator: {
name: 'Criador de Conteúdo',
description: 'Criou 50 highlights',
requirement: { highlightsCreated: 50 }
},
popular: {
name: 'Popular',
description: 'Recebeu 100 likes em highlights',
requirement: { totalLikes: 100 }
}
}
class HighlightGamification {
async checkBadges(playerId: string): Promise<Badge[]> {
const player = await this.playerService.getPlayer(playerId);
const highlights = await this.highlightService.getPlayerHighlights(playerId);
const newBadges: Badge[] = [];
// Viral
const viral = highlights.find(h => h.views >= 1000);
if (viral && !player.badges.includes('viral')) {
newBadges.push(await this.badgeService.award(playerId, 'viral'));
}
// Superstar
const superstar = highlights.find(h => h.views >= 10000);
if (superstar && !player.badges.includes('superstar')) {
newBadges.push(await this.badgeService.award(playerId, 'superstar'));
}
// Creator
if (highlights.length >= 50 && !player.badges.includes('creator')) {
newBadges.push(await this.badgeService.award(playerId, 'creator'));
}
// Popular
const totalLikes = highlights.reduce((sum, h) => sum + h.likes, 0);
if (totalLikes >= 100 && !player.badges.includes('popular')) {
newBadges.push(await this.badgeService.award(playerId, 'popular'));
}
return newBadges;
}
// XP por engagement
async rewardEngagement(highlightId: string): Promise<void> {
const highlight = await this.highlightService.getHighlight(highlightId);
// XP por likes
if (highlight.likes >= 10) {
await this.xpService.add(highlight.playerId, 50, {
source: 'highlight_likes',
highlightId
});
}
// XP por views
if (highlight.views >= 100) {
await this.xpService.add(highlight.playerId, 100, {
source: 'highlight_views',
highlightId
});
}
// Bonus por viralização
if (highlight.views >= 1000) {
await this.xpService.add(highlight.playerId, 500, {
source: 'highlight_viral',
highlightId
});
}
}
}10. Métricas de Engajamento
10.1 Core Metrics
interface EngagementMetrics {
// DAU, WAU, MAU
dau: number; // Daily Active Users
wau: number; // Weekly Active Users
mau: number; // Monthly Active Users
// Stickiness
stickiness: number; // DAU/MAU ratio (ideal: 20%+)
// Retention
d1Retention: number; // Voltam no dia seguinte
d7Retention: number; // Voltam na primeira semana
d30Retention: number; // Voltam no primeiro mês
// Session
avgSessionTime: number; // Minutos
avgSessionsPerDay: number;
avgMatchesPerSession: number;
// Engagement
xpEarnedPerDay: number;
badgesUnlockedPerDay: number;
matchesPlayedPerDay: number;
// Monetização (ArenaCoins)
arenaCoinsSpent: number;
arenaCoinsEarned: number;
arenaCoinsBalance: number;
purchaseConversionRate: number; // % que compraram coins
// Social
friendInviteRate: number;
highlightShareRate: number;
matchesWithFriends: number;
}
class AnalyticsService {
// Calcular DAU
async calculateDAU(date: Date): Promise<number> {
const startOfDay = startOfDay(date);
const endOfDay = endOfDay(date);
return await this.userRepository
.where('lastActiveAt')
.between(startOfDay, endOfDay)
.count();
}
// Calcular stickiness
async calculateStickiness(date: Date): Promise<number> {
const dau = await this.calculateDAU(date);
const mau = await this.calculateMAU(date);
return (dau / mau) * 100;
}
// Calcular retention
async calculateRetention(cohortDate: Date, days: number): Promise<number> {
// Usuários que fizeram signup em cohortDate
const cohort = await this.userRepository
.where('createdAt')
.between(startOfDay(cohortDate), endOfDay(cohortDate))
.toArray();
// Quantos voltaram após N dias
const targetDate = addDays(cohortDate, days);
const retained = cohort.filter(user =>
this.wasActiveOn(user, targetDate)
);
return (retained.length / cohort.length) * 100;
}
// Session time
async calculateAvgSessionTime(userId: string, period: Date): Promise<number> {
const sessions = await this.sessionRepository
.where('userId').equals(userId)
.where('startedAt').gte(period)
.toArray();
const totalTime = sessions.reduce((sum, s) => sum + s.duration, 0);
return totalTime / sessions.length;
}
}10.2 Funnel Metrics
interface OnboardingFunnel {
// Step 1: Sign up
signups: number;
// Step 2: Complete profile
profileCompleted: number;
profileCompletionRate: number;
// Step 3: First match
firstMatch: number;
firstMatchRate: number;
// Step 4: Second session
secondSession: number;
secondSessionRate: number;
// Step 5: Week 1 retention
week1Retention: number;
week1RetentionRate: number;
// Drop-off points
dropoffs: {
afterSignup: number;
afterProfile: number;
afterFirstMatch: number;
afterSecondSession: number;
};
}
class FunnelAnalytics {
async analyzeFunnel(startDate: Date, endDate: Date): Promise<OnboardingFunnel> {
const signups = await this.getSignups(startDate, endDate);
const funnel: OnboardingFunnel = {
signups: signups.length,
profileCompleted: 0,
profileCompletionRate: 0,
firstMatch: 0,
firstMatchRate: 0,
secondSession: 0,
secondSessionRate: 0,
week1Retention: 0,
week1RetentionRate: 0,
dropoffs: {
afterSignup: 0,
afterProfile: 0,
afterFirstMatch: 0,
afterSecondSession: 0
}
};
for (const user of signups) {
// Step 2: Profile
if (user.profileCompleted) {
funnel.profileCompleted++;
// Step 3: First match
if (user.matchesPlayed >= 1) {
funnel.firstMatch++;
// Step 4: Second session
if (user.sessions >= 2) {
funnel.secondSession++;
// Step 5: Week 1
if (this.wasActiveInFirstWeek(user)) {
funnel.week1Retention++;
} else {
funnel.dropoffs.afterSecondSession++;
}
} else {
funnel.dropoffs.afterFirstMatch++;
}
} else {
funnel.dropoffs.afterProfile++;
}
} else {
funnel.dropoffs.afterSignup++;
}
}
// Calcular rates
funnel.profileCompletionRate = (funnel.profileCompleted / funnel.signups) * 100;
funnel.firstMatchRate = (funnel.firstMatch / funnel.profileCompleted) * 100;
funnel.secondSessionRate = (funnel.secondSession / funnel.firstMatch) * 100;
funnel.week1RetentionRate = (funnel.week1Retention / funnel.secondSession) * 100;
return funnel;
}
}10.3 Cohort Analysis
interface CohortAnalysis {
cohortDate: Date;
cohortSize: number;
// Retention por período
retention: {
day1: number;
day3: number;
day7: number;
day14: number;
day30: number;
day60: number;
day90: number;
};
// Engagement por período
engagement: {
avgMatchesPerUser: number;
avgXPEarned: number;
avgSessionTime: number;
};
// Monetização
monetization: {
payers: number;
payerRate: number;
arpu: number; // Average Revenue Per User
arppu: number; // Average Revenue Per Paying User
};
}
class CohortAnalytics {
async analyzeCohort(cohortDate: Date): Promise<CohortAnalysis> {
const cohort = await this.getCohort(cohortDate);
const analysis: CohortAnalysis = {
cohortDate,
cohortSize: cohort.length,
retention: {
day1: await this.calculateRetention(cohort, 1),
day3: await this.calculateRetention(cohort, 3),
day7: await this.calculateRetention(cohort, 7),
day14: await this.calculateRetention(cohort, 14),
day30: await this.calculateRetention(cohort, 30),
day60: await this.calculateRetention(cohort, 60),
day90: await this.calculateRetention(cohort, 90)
},
engagement: await this.calculateEngagement(cohort),
monetization: await this.calculateMonetization(cohort)
};
return analysis;
}
// Visualização de cohort
async generateCohortTable(startDate: Date, endDate: Date): Promise<CohortTable> {
const cohorts: CohortAnalysis[] = [];
let currentDate = startDate;
while (currentDate <= endDate) {
cohorts.push(await this.analyzeCohort(currentDate));
currentDate = addDays(currentDate, 7); // Cohorts semanais
}
return {
cohorts,
avgRetention: this.calculateAverageRetention(cohorts),
bestCohort: this.findBestCohort(cohorts),
worstCohort: this.findWorstCohort(cohorts)
};
}
}10.4 Feature Adoption
interface FeatureAdoption {
featureName: string;
launchDate: Date;
// Adoção
totalUsers: number;
adoptedUsers: number;
adoptionRate: number;
// Tempo para adoção
avgTimeToAdopt: number; // Dias
// Engagement
avgUsagePerUser: number;
dailyActiveInFeature: number;
// Por segmento
byLevel: Map<number, FeatureUsage>;
byTenure: Map<string, FeatureUsage>;
}
interface FeatureUsage {
users: number;
adoptionRate: number;
avgUsage: number;
}
class FeatureAnalytics {
// Analisar adoção de feature
async analyzeFeature(featureName: string): Promise<FeatureAdoption> {
const feature = await this.featureRepository.findByName(featureName);
const users = await this.userRepository.findAll();
const adopted = users.filter(u =>
this.hasUsedFeature(u, featureName)
);
return {
featureName,
launchDate: feature.launchDate,
totalUsers: users.length,
adoptedUsers: adopted.length,
adoptionRate: (adopted.length / users.length) * 100,
avgTimeToAdopt: this.calculateAvgTimeToAdopt(adopted, feature),
avgUsagePerUser: this.calculateAvgUsage(adopted, featureName),
dailyActiveInFeature: await this.getDailyActive(featureName),
byLevel: await this.getAdoptionByLevel(featureName),
byTenure: await this.getAdoptionByTenure(featureName)
};
}
// Comparar features
async compareFeatures(features: string[]): Promise<FeatureComparison> {
const analyses = await Promise.all(
features.map(f => this.analyzeFeature(f))
);
return {
features: analyses,
mostAdopted: this.findMostAdopted(analyses),
leastAdopted: this.findLeastAdopted(analyses),
fastestAdoption: this.findFastestAdoption(analyses),
recommendations: this.generateRecommendations(analyses)
};
}
}10.5 Dashboards
interface GamificationDashboard {
// Overview
overview: {
totalPlayers: number;
activePlayers: number;
totalMatches: number;
totalXPEarned: number;
totalBadgesUnlocked: number;
};
// Engagement
engagement: {
dau: number;
wau: number;
mau: number;
stickiness: number;
avgSessionTime: number;
matchesPerUser: number;
};
// Progression
progression: {
avgLevel: number;
levelDistribution: Map<number, number>;
xpEarnedToday: number;
badgesUnlockedToday: number;
};
// Social
social: {
highlightsCreated: number;
highlightsShared: number;
friendInvites: number;
activeFriendships: number;
};
// Economy
economy: {
arenaCoinsInCirculation: number;
arenaCoinsSpentToday: number;
arenaCoinsEarnedToday: number;
topSpenders: Player[];
};
// Top performers
leaderboards: {
topXP: Player[];
topLevel: Player[];
topWinRate: Player[];
topStreak: Player[];
};
// Alerts
alerts: Alert[];
}
class DashboardService {
async generateDashboard(date: Date): Promise<GamificationDashboard> {
return {
overview: await this.getOverview(),
engagement: await this.getEngagement(date),
progression: await this.getProgression(date),
social: await this.getSocial(date),
economy: await this.getEconomy(date),
leaderboards: await this.getLeaderboards(),
alerts: await this.getAlerts()
};
}
private async getAlerts(): Promise<Alert[]> {
const alerts: Alert[] = [];
// Queda no DAU
const dauToday = await this.analyticsService.calculateDAU(new Date());
const dauYesterday = await this.analyticsService.calculateDAU(subDays(new Date(), 1));
if (dauToday < dauYesterday * 0.9) {
alerts.push({
type: 'warning',
message: `DAU dropped ${((1 - dauToday/dauYesterday) * 100).toFixed(1)}%`,
severity: 'high'
});
}
// Retention baixa
const d1Retention = await this.analyticsService.calculateRetention(subDays(new Date(), 1), 1);
if (d1Retention < 40) {
alerts.push({
type: 'warning',
message: `D1 retention is ${d1Retention.toFixed(1)}% (target: 40%+)`,
severity: 'high'
});
}
return alerts;
}
}11. Implementação Técnica
11.1 Entidades de Domínio
// Player Gamification Profile
interface PlayerGamification {
playerId: string;
// Progressão
level: number;
currentXP: number;
totalXP: number;
title: string;
// Stats
matchesPlayed: number;
wins: number;
losses: number;
winRate: number;
// Rating
rating: number;
ratingHistory: RatingHistory[];
// Badges
badges: Badge[];
badgeProgress: BadgeProgress[];
// Streaks
loginStreak: Streak;
winStreak: Streak;
// Challenges
activeChallenges: Challenge[];
completedChallenges: Challenge[];
// Skills
skills: Map<string, Skill>;
// Wallet
walletId: string;
// Preferences
preferences: {
showInLeaderboard: boolean;
showStats: boolean;
notifications: NotificationPreferences;
};
// Timestamps
createdAt: Date;
updatedAt: Date;
lastActiveAt: Date;
}
// Badge Progress (para badges em progresso)
interface BadgeProgress {
badgeId: string;
currentProgress: number;
targetProgress: number;
progressPercent: number;
unlockedAt?: Date;
}
// Rating History (para gráficos)
interface RatingHistory {
rating: number;
change: number;
matchId: string;
timestamp: Date;
}11.2 Eventos
// Domain Events
enum GamificationEvent {
// XP
XP_EARNED = 'xp.earned',
LEVEL_UP = 'level.up',
// Badges
BADGE_UNLOCKED = 'badge.unlocked',
BADGE_PROGRESS = 'badge.progress',
// Challenges
CHALLENGE_ASSIGNED = 'challenge.assigned',
CHALLENGE_PROGRESS = 'challenge.progress',
CHALLENGE_COMPLETED = 'challenge.completed',
// Streaks
STREAK_STARTED = 'streak.started',
STREAK_CONTINUED = 'streak.continued',
STREAK_MILESTONE = 'streak.milestone',
STREAK_BROKEN = 'streak.broken',
// Wallet
COINS_EARNED = 'coins.earned',
COINS_SPENT = 'coins.spent',
COINS_TRANSFERRED = 'coins.transferred',
// Match
MATCH_COMPLETED = 'match.completed',
MATCH_WON = 'match.won',
MATCH_MVP = 'match.mvp',
// Social
HIGHLIGHT_CREATED = 'highlight.created',
HIGHLIGHT_SHARED = 'highlight.shared',
FRIEND_INVITED = 'friend.invited',
// Ranking
RANK_CHANGED = 'rank.changed',
RANK_MILESTONE = 'rank.milestone'
}
interface Event<T = any> {
id: string;
type: GamificationEvent;
aggregateId: string; // playerId
data: T;
timestamp: Date;
metadata?: object;
}
// Event Bus
class EventBus {
private handlers = new Map<GamificationEvent, EventHandler[]>();
subscribe(event: GamificationEvent, handler: EventHandler): void {
if (!this.handlers.has(event)) {
this.handlers.set(event, []);
}
this.handlers.get(event)!.push(handler);
}
async publish<T>(event: GamificationEvent, data: T): Promise<void> {
const handlers = this.handlers.get(event) || [];
await Promise.all(
handlers.map(handler => handler.handle({
id: uuidv4(),
type: event,
aggregateId: data.playerId || data.userId,
data,
timestamp: new Date()
}))
);
}
}11.3 Rule Engine
// Rule Engine para badges e achievements
interface Rule {
id: string;
name: string;
description: string;
conditions: Condition[];
action: Action;
priority: number;
}
interface Condition {
type: 'equals' | 'greaterThan' | 'lessThan' | 'between' | 'contains';
field: string;
value: any;
operator?: 'and' | 'or';
}
interface Action {
type: 'award_badge' | 'add_xp' | 'add_coins' | 'unlock_feature';
params: object;
}
class RuleEngine {
private rules: Rule[] = [];
registerRule(rule: Rule): void {
this.rules.push(rule);
this.rules.sort((a, b) => b.priority - a.priority);
}
async evaluate(player: Player, context: Context): Promise<Action[]> {
const actions: Action[] = [];
for (const rule of this.rules) {
if (await this.evaluateConditions(rule.conditions, player, context)) {
actions.push(rule.action);
}
}
return actions;
}
private async evaluateConditions(
conditions: Condition[],
player: Player,
context: Context
): Promise<boolean> {
for (const condition of conditions) {
const value = this.getFieldValue(player, context, condition.field);
const result = this.evaluateCondition(condition, value);
if (!result) return false;
}
return true;
}
private evaluateCondition(condition: Condition, value: any): boolean {
switch (condition.type) {
case 'equals':
return value === condition.value;
case 'greaterThan':
return value > condition.value;
case 'lessThan':
return value < condition.value;
case 'between':
return value >= condition.value[0] && value <= condition.value[1];
case 'contains':
return Array.isArray(value) && value.includes(condition.value);
default:
return false;
}
}
}
// Exemplo: Registrar regras para badges
const badgeRules: Rule[] = [
{
id: 'first_match_badge',
name: 'First Match Badge',
description: 'Award badge after first match',
conditions: [
{ type: 'equals', field: 'matchesPlayed', value: 1 }
],
action: {
type: 'award_badge',
params: { badgeId: 'first_match' }
},
priority: 100
},
{
id: 'level_10_badge',
name: 'Level 10 Badge',
description: 'Award badge when reaching level 10',
conditions: [
{ type: 'equals', field: 'level', value: 10 }
],
action: {
type: 'award_badge',
params: { badgeId: 'rising_star' }
},
priority: 90
}
];11.4 APIs
// REST API Endpoints
// GET /api/gamification/profile
interface GetProfileResponse {
player: PlayerGamification;
nextLevel: {
level: number;
xpRequired: number;
xpRemaining: number;
estimatedMatches: number;
};
recentAchievements: Achievement[];
}
// GET /api/gamification/leaderboard
interface GetLeaderboardRequest {
type?: RankingType;
period?: RankingPeriod;
sport?: string;
limit?: number;
offset?: number;
}
interface GetLeaderboardResponse {
rankings: Ranking[];
userRank?: Ranking;
totalPlayers: number;
}
// GET /api/gamification/badges
interface GetBadgesResponse {
earned: Badge[];
inProgress: BadgeProgress[];
available: Badge[];
locked: Badge[];
}
// GET /api/gamification/challenges
interface GetChallengesResponse {
daily: Challenge[];
weekly: Challenge[];
special: Challenge[];
progress: Map<string, number>;
}
// POST /api/gamification/highlight
interface CreateHighlightRequest {
matchId: string;
timestamp: number;
type: HighlightType;
description?: string;
}
interface CreateHighlightResponse {
highlight: Highlight;
estimatedProcessingTime: number;
}
// POST /api/gamification/match/staked
interface CreateStakedMatchRequest {
sport: string;
type: MatchType;
entryFee: number;
maxPlayers: number;
minLevel?: number;
rules: MatchRules;
}
interface CreateStakedMatchResponse {
match: StakedMatch;
yourEntry: {
frozen: number;
balance: number;
};
}
// WebSocket Events
interface WebSocketEvents {
// Real-time updates
'xp:earned': { amount: number; total: number };
'level:up': { level: number; title: string };
'badge:unlocked': Badge;
'challenge:progress': { challengeId: string; progress: number };
'rank:changed': { oldRank: number; newRank: number };
'highlight:ready': Highlight;
'match:started': StakedMatch;
'match:completed': { match: StakedMatch; prizes: Prize[] };
}11.5 Processamento Assíncrono
// Job Queue para tarefas pesadas
interface Job {
id: string;
type: JobType;
data: any;
priority: number;
attempts: number;
maxAttempts: number;
createdAt: Date;
processedAt?: Date;
}
enum JobType {
PROCESS_HIGHLIGHT = 'process_highlight',
CALCULATE_RANKINGS = 'calculate_rankings',
AWARD_BADGES = 'award_badges',
GENERATE_CHALLENGES = 'generate_challenges',
UPDATE_STATS = 'update_stats',
SEND_NOTIFICATIONS = 'send_notifications'
}
class JobQueue {
async enqueue(job: Omit<Job, 'id' | 'attempts' | 'createdAt'>): Promise<string> {
const jobId = uuidv4();
await this.jobRepository.create({
...job,
id: jobId,
attempts: 0,
createdAt: new Date()
});
// Notificar worker
await this.redis.publish('jobs:new', jobId);
return jobId;
}
async process(): Promise<void> {
while (true) {
const job = await this.jobRepository.getNext();
if (!job) {
await this.sleep(1000);
continue;
}
try {
await this.processJob(job);
await this.jobRepository.markCompleted(job.id);
} catch (error) {
job.attempts++;
if (job.attempts >= job.maxAttempts) {
await this.jobRepository.markFailed(job.id, error);
} else {
await this.jobRepository.retry(job.id);
}
}
}
}
private async processJob(job: Job): Promise<void> {
const handlers = {
[JobType.PROCESS_HIGHLIGHT]: this.processHighlight,
[JobType.CALCULATE_RANKINGS]: this.calculateRankings,
[JobType.AWARD_BADGES]: this.awardBadges,
[JobType.GENERATE_CHALLENGES]: this.generateChallenges,
[JobType.UPDATE_STATS]: this.updateStats,
[JobType.SEND_NOTIFICATIONS]: this.sendNotifications
};
const handler = handlers[job.type];
if (!handler) {
throw new Error(`Unknown job type: ${job.type}`);
}
await handler.call(this, job.data);
}
}Conclusão
Este documento especifica um sistema completo de gamificação baseado em princípios sólidos de neurociência e psicologia comportamental, garantindo:
- Engajamento Saudável: Sem dark patterns, com limites e proteções
- Progressão Balanceada: Curva de XP justa, múltiplas formas de progressão
- Competição Não-Punitiva: Rankings contextuais, celebração de pequenas vitórias
- Economia Sustentável: ArenaCoins com fontes e sinks balanceados
- Social e Viral: Highlights compartilháveis, conquistas sociais
- Métricas Robustas: Dashboards e analytics para otimização contínua
Próximos Passos:
- Implementação em fases (MVP → Full)
- A/B testing de mecânicas
- Balanceamento contínuo baseado em dados
- Expansão de badges e desafios baseado em feedback
Documento criado por: Escriba (Technical Writer) Data: 2024-01-15 Versão: 1.0