# US-GAM-004: Sistema de ayudas **Épica:** EAI-003 - Gamificación Básica **Sprint:** Mes 1, Semana 3 **Story Points:** 7 SP **Presupuesto:** $2,600 MXN **Prioridad:** Alta (Alcance Inicial) **Estado:** ✅ Completada (Mes 1) --- ## Descripción Como **estudiante**, quiero **usar ayudas durante actividades difíciles** para **facilitar mi aprendizaje gastando monedas**. **Contexto del Alcance Inicial:** 3 tipos de ayudas con costos fijos en monedas. Solo disponibles en actividades de opción múltiple y verdadero/falso. --- ## Criterios de Aceptación - [ ] **CA-01:** 3 ayudas: Pista (5 coins), Eliminar opción (10 coins), Tiempo extra (15 coins) - [ ] **CA-02:** Botones de ayudas visibles durante actividad - [ ] **CA-03:** Se muestra costo en monedas - [ ] **CA-04:** Confirmación antes de usar (gastar monedas) - [ ] **CA-05:** Ayuda "Pista" muestra hint educativo - [ ] **CA-06:** Ayuda "Eliminar opción" tacha 1-2 opciones incorrectas - [ ] **CA-07:** Ayuda "Tiempo extra" agrega +30 segundos (si hay timer) - [ ] **CA-08:** No se pueden usar si no hay monedas suficientes - [ ] **CA-09:** Máximo 1 ayuda de cada tipo por actividad - [ ] **CA-10:** Se registra uso de ayudas en analytics --- ## Especificaciones Técnicas ### Backend ```typescript enum HelpType { HINT = 'hint', REMOVE_OPTION = 'remove_option', EXTRA_TIME = 'extra_time' } const HELP_COSTS = { [HelpType.HINT]: 5, [HelpType.REMOVE_OPTION]: 10, [HelpType.EXTRA_TIME]: 15 } @Entity('help_usage') class HelpUsage { @PrimaryGeneratedColumn('uuid') id: string @Column() userId: string @Column() activityId: string @Column({ type: 'enum', enum: HelpType }) helpType: HelpType @Column({ type: 'int' }) coinsCost: number @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) usedAt: Date } class HelpService { async useHelp(userId: string, activityId: string, helpType: HelpType) { // Verificar que no se haya usado antes en esta actividad const existingUsage = await this.helpUsageRepository.findOne({ where: { userId, activityId, helpType } }) if (existingUsage) { throw new BadRequestException('Help already used in this activity') } const cost = HELP_COSTS[helpType] // Gastar monedas await this.coinsService.spendCoins(userId, cost, 'help_used') // Registrar uso await this.helpUsageRepository.save({ userId, activityId, helpType, coinsCost: cost }) // Retornar resultado según tipo switch (helpType) { case HelpType.HINT: return await this.getHint(activityId) case HelpType.REMOVE_OPTION: return await this.getOptionsToRemove(activityId) case HelpType.EXTRA_TIME: return { extraSeconds: 30 } } } private async getHint(activityId: string) { const activity = await this.activitiesRepository.findOne({ where: { id: activityId } }) return { hint: activity.content.hint || 'No hay pista disponible' } } private async getOptionsToRemove(activityId: string) { const activity = await this.activitiesRepository.findOne({ where: { id: activityId } }) if (activity.type !== ActivityType.MULTIPLE_CHOICE) { throw new BadRequestException('Help not available for this activity type') } const incorrectOptions = activity.content.options .filter(opt => !opt.isCorrect) .slice(0, 2) // Eliminar hasta 2 opciones incorrectas return { optionsToRemove: incorrectOptions.map(o => o.id) } } } ``` **Endpoints:** ``` POST /api/activities/:activityId/help/use - Body: { helpType: 'hint' | 'remove_option' | 'extra_time' } - Response: { hint?, optionsToRemove?, extraSeconds? } GET /api/activities/:activityId/help/available - Response: { helps: [ { type: 'hint', cost: 5, used: false, available: true }, { type: 'remove_option', cost: 10, used: false, available: true }, { type: 'extra_time', cost: 15, used: true, available: false } ], userCoins: 45 } ``` ### Frontend ```typescript // components/activities/HelpButtons.tsx export function HelpButtons({ activityId, userCoins, onHelpUsed }) { const [helps, setHelps] = useState([]) const [loading, setLoading] = useState(false) useEffect(() => { loadAvailableHelps() }, [activityId]) const handleUseHelp = async (helpType: HelpType) => { const help = helps.find(h => h.type === helpType) if (userCoins < help.cost) { toast.error('No tienes suficientes monedas') return } if (!window.confirm(`¿Usar ${help.type} por ${help.cost} monedas?`)) { return } setLoading(true) try { const result = await activitiesService.useHelp(activityId, helpType) onHelpUsed(helpType, result) await loadAvailableHelps() // Recargar disponibilidad toast.success('Ayuda aplicada') } catch (error) { toast.error(error.message) } finally { setLoading(false) } } return (
💡 {hint}