Sistema de XP
Visión general
Section titled “Visión general”El sistema de XP (puntos de experiencia) asigna valor numérico al progreso del estudiante en la plataforma. Su propósito es doble:
- Nivel personal — Cada estudiante tiene un nivel del 1 al 10 con un nombre de rango. El nivel se muestra en el dashboard y en el portfolio público con una barra de progreso hacia el siguiente umbral.
- Criterio de badges — Los badges de categoría
LEVEL(y algunos mixtos) usan el XP acumulado como condición de desbloqueo automático.
No hay leaderboard público ni desbloqueo de contenido por XP; el acceso al contenido se gobierna exclusivamente por el AccessLevelGuard basado en el plan de suscripción del usuario.
Fuentes de XP
Section titled “Fuentes de XP”El XP total de un estudiante se calcula como la suma de eventos XP más la contribución de badges activos por tier.
Eventos de aprendizaje
Section titled “Eventos de aprendizaje”Evento (eventType) | XP otorgado | Módulo emisor | Condición |
|---|---|---|---|
LESSON_COMPLETED | +10 XP | ProgressModule | Primera vez que se marca una lección como completada |
ROUTE_COMPLETED | +100 XP | ProgressModule | Todas las lecciones de un path marcadas como completadas |
QUIZ_PASSED | +20 XP | QuizModule | Intento de quiz con passed = true |
QUIZ_PERFECT | +25 XP | QuizModule | Intento de quiz con score = 100 (acumulable con QUIZ_PASSED) |
Contribución de badges por tier
Section titled “Contribución de badges por tier”Los badges activos del estudiante (no revocados) aportan XP adicional según su tier. Esta contribución no genera un StudentXPEvent — se suma al vuelo al calcular el total.
| Tier del badge | XP aportado |
|---|---|
bronze (tier 1) | +25 XP |
silver (tier 2) | +50 XP |
gold (tier 3) | +100 XP |
platinum (tier 4) | +200 XP |
legend (tier 5) | +500 XP |
Fórmula de cálculo
Section titled “Fórmula de cálculo”xpTotal = Σ(StudentXPEvent.xp) + Σ(BADGE_TIER_XP[badge.tier])Donde BADGE_TIER_XP = { 1: 25, 2: 50, 3: 100, 4: 200, 5: 500 }.
No existen multiplicadores, streaks de XP ni bonificaciones temporales. La fórmula es idéntica en XpService.getStudentXPSummary(), PortfolioService.getPublicPortfolio() y StudentBadgesService.calculateStats(), garantizando que dashboard y portfolio público muestren siempre el mismo total.
Niveles
Section titled “Niveles”El nivel se deriva del XP total con la función xpToLevel:
nivel = min(10, max(1, floor(xpTotal / 500) + 1))Esto significa 500 XP por nivel, con máximo en el nivel 10.
| Nivel | XP mínimo | XP máximo del tramo | Nombre de rango |
|---|---|---|---|
| 1 | 0 | 499 | Aprendiz IA |
| 2 | 500 | 999 | Aprendiz IA |
| 3 | 1 000 | 1 499 | Aprendiz IA |
| 4 | 1 500 | 1 999 | Practicante IA |
| 5 | 2 000 | 2 499 | Practicante IA |
| 6 | 2 500 | 2 999 | Arquitecto IA |
| 7 | 3 000 | 3 499 | Arquitecto IA |
| 8 | 3 500 | 3 999 | Experto IA |
| 9 | 4 000 | 4 499 | Experto IA |
| 10 | 4 500 | ∞ | Maestro IA |
Los umbrales del tramo actual (xpCurrentLevel, xpNextLevel) se calculan como:
xpCurrentLevel = (nivel - 1) * 500xpNextLevel = nivel * 500Al alcanzar el nivel 10 (xpTotal ≥ 4 500), el progreso se muestra al 100% y se omite el mensaje “XP para el siguiente nivel”.
Diagrama — Flujo de otorgamiento de XP
Section titled “Diagrama — Flujo de otorgamiento de XP”flowchart TD A[Estudiante] -->|completa lección| B[ProgressService.completeLesson] A -->|completa path| C[ProgressService — path completion] A -->|envía quiz| D[QuizService.submitAttempt]
B -->|emitEvent LESSON_COMPLETED +10| E[XpService.emitEvent] C -->|emitEvent ROUTE_COMPLETED +100| E D -->|passed=true → emitEvent QUIZ_PASSED +20| E D -->|score=100 → emitEvent QUIZ_PERFECT +25| E
E -->|INSERT StudentXPEvent| F[(BD: StudentXPEvent)]
G[Dashboard / Portfolio] -->|getStudentXPSummary| H[XpService] H -->|SELECT StudentXPEvent| F H -->|SELECT StudentBadgeGrant activos| I[(BD: StudentBadgeGrant)] H -->|xpTotal = eventos + badges| J[XPSummary] J -->|nivel, levelName, thresholds| GServicio XP — API interna
Section titled “Servicio XP — API interna”Archivo: apps/api/src/modules/xp/xp.service.ts
Módulo: XpModule (no es @Global() — los módulos consumidores deben importar XpModule)
emitEvent(userId, eventType, xp, metadata?)
Section titled “emitEvent(userId, eventType, xp, metadata?)”Registra un evento XP. Operación fire-and-forget: nunca lanza excepciones al llamador; los errores se loguean internamente.
// Ejemplo de uso desde ProgressServicethis.xpService.emitEvent(userId, 'LESSON_COMPLETED', 10).catch(() => {})
// Con metadata opcional (queda en StudentXPEvent.metadata)this.xpService.emitEvent(userId, 'QUIZ_PASSED', 20, { quizId, attemptId }).catch(() => {})getStudentXPSummary(userId): Promise<XPSummary>
Section titled “getStudentXPSummary(userId): Promise<XPSummary>”Calcula y devuelve el resumen XP completo del estudiante:
interface XPSummary { xpTotal: number // XP acumulado total (eventos + badges) currentLevel: number // Nivel actual 1–10 levelName: string // Nombre del rango (ej. "Practicante IA") xpCurrentLevel: number // Umbral inferior del tramo actual, ej. 1500 xpNextLevel: number // Umbral superior del tramo actual, ej. 2000}Tipo XpEventType
Section titled “Tipo XpEventType”type XpEventType = | 'LESSON_COMPLETED' | 'ROUTE_COMPLETED' | 'BADGE_EARNED' // Reservado — no emitido actualmente; las badges aportan XP por tier | 'QUIZ_PASSED' | 'QUIZ_PERFECT'Módulos que consumen XpModule
Section titled “Módulos que consumen XpModule”| Módulo | Operación |
|---|---|
ProgressModule | emitEvent al completar lección o path |
QuizModule | emitEvent al superar un quiz |
DashboardModule | getStudentXPSummary para el widget del dashboard |
StudentBadgesModule | calculateStudentXP auxiliar en evaluador de badges |
Modelo en base de datos
Section titled “Modelo en base de datos”StudentXPEvent
Section titled “StudentXPEvent”Historial de eventos individuales. Cada fila representa una ganancia de XP.
model StudentXPEvent { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id]) eventType String // 'LESSON_COMPLETED' | 'ROUTE_COMPLETED' | 'QUIZ_PASSED' | 'QUIZ_PERFECT' xp Int // Valor positivo del evento metadata Json? // Contexto opcional: { quizId, attemptId, pathId, source: 'backfill' } createdAt DateTime @default(now())
@@index([userId])}No existe un campo xpTotal desnormalizado en el modelo User. El total siempre se calcula sumando los eventos en tiempo real, más la contribución de badges activos.
Frontend — Dónde se muestra el XP
Section titled “Frontend — Dónde se muestra el XP”Widget del dashboard (/dashboard)
Section titled “Widget del dashboard (/dashboard)”El componente inline en DashboardPage.tsx muestra:
- Círculo con número de nivel (gradiente naranja)
- XP total en tipografía grande
- Nombre del rango (ej. “Practicante IA”)
- Barra de progreso del tramo actual (
xpTotal - xpCurrentLevel/xpNextLevel - xpCurrentLevel) - XP restantes para el siguiente nivel
Datos servidos por GET /api/dashboard → DashboardService.getSummary() → incluye xpTotal, currentLevel, levelName, xpCurrentLevel, xpNextLevel en el objeto stats.
Portfolio público (/u/:username)
Section titled “Portfolio público (/u/:username)”El componente XPMeter en apps/web/src/components/portfolio/XPMeter.tsx es visible en el portfolio público del estudiante si settings.showXP = true. Muestra el XP total, nivel, barra de progreso y, si rankPercentile > 0, el percentil en la plataforma.
Los datos provienen de GET /api/portfolio/:username → PortfolioService.getPublicPortfolio(), que calcula el XP con la misma fórmula que XpService.
Resumen de badges (/dashboard/badges)
Section titled “Resumen de badges (/dashboard/badges)”BadgeSummary (devuelto por GET /api/student-badges/summary) incluye el campo xp calculado por StudentBadgesService.calculateStats(). Se usa para determinar el progreso hacia el siguiente badge de nivel.