Diario del desarrollador
Esta sección es para quienes tienen curiosidad sobre cómo se construyó CONTRABAND. Es un proyecto en solitario desarrollado durante varios meses usando únicamente JavaScript vanilla, CSS y HTML — sin motor de juego, sin frameworks pesados. El juego entero corre en una pestaña del navegador. Esta serie de notas documenta las decisiones arquitectónicas, los compromisos creativos y lo que aprendí en el camino.
¿Por qué no usar un motor?
La elección obvia para un juego de navegador habría sido Phaser, Three.js en bruto o un export HTML5 de Godot. Elegí vanilla por cuatro razones concretas:
- Tamaño de archivo. El juego completo pesa menos de 120KB de código. Incluso con los assets, el navegador lo carga instantáneamente. Un build WebGL de Unity ocuparía 20-50MB.
- Velocidad de arranque. De URL a jugable en menos de 2 segundos en un móvil moderno. Sin calentamiento del motor.
- Arquitectura modular. Cada sistema es su propio archivo con fronteras limpias. El combate no sabe de historias. Las historias no saben de combate. Se comunican vía EventBus.
- Cero lock-in de vendor. Si un framework muere, no pierdo nada. Los estándares web no rompen.
Arquitectura
El juego tiene cinco capas conceptuales organizadas por responsabilidad y nivel de dependencia:
- Core — EventBus, State, secuencia de arranque. ~500 líneas. Ningún otro sistema escribe aquí; todos leen.
- Data — Archivos JS puros con datos declarativos: naves, objetos, tripulantes, historia, logros. ~3.500 líneas.
- Systems — Combate, Galaxia, Timeline, Mercado, Motor de historia, Tripulación, Monetización, Analytics, Audio. ~2.000 líneas.
- UI — HUD, modales, toasts, paneles para cada sistema. ~1.500 líneas.
- CSS — Sistema de diseño + estilos de componentes + responsive móvil. ~2.500 líneas.
Total: aproximadamente 10.000 líneas repartidas en 50+ archivos.
Decisiones de diseño clave
EventBus sobre acoplamiento directo
Cada sistema emite eventos (combat:victory, story:decision, galaxy:jump) en lugar de llamar directamente a otros sistemas. Esto significa que agregar una nueva funcionalidad — como Analytics — es simplemente "escuchar los eventos existentes". Cero refactor del resto del código. Cuando añadimos el sistema de monetización a mitad del desarrollo, no tuvimos que tocar combate ni historia: solo escuchar story:scene-unlocked para verificar si era un epilogue pagado.
State como única fuente de verdad
Hay una sola instancia GameState. Todo lee de ella. Guardar es literalmente JSON.stringify(state). Cargar es JSON.parse + Object.assign. Sin reducers complejos, sin middleware. Esto hizo que el sistema de guardado cross-device con Firestore fuera trivial: el payload subido a la nube es el mismo JSON que se guarda en localStorage.
Audio procedural
No licencié música. El soundtrack ambiental son seis ondas sinusoidales superpuestas con modulación LFO, generadas en tiempo de ejecución por la Web Audio API. Cuesta 0 de bandwidth y 0 dólares. Los efectos de disparo, explosiones, warp y UI también son procedurales: un oscilador + envelope + filtro, configurados por preset. El archivo de audio pesa exactamente 0 bytes.
Retratos ASCII
Los retratos de los personajes son arte ASCII, no imágenes. Cada NPC tiene un patrón de 4 líneas renderizado en el color dorado de acento. Fue una decisión creativa que encaja con la estética retro-terminal del juego, pero también es práctica: cero assets de imagen, cero tiempo de carga.
Firestore como autoridad, localStorage como caché
Los entitlements de compras (los epilogues pagos, los cosméticos) se guardan en Firestore vía webhooks de Stripe. Pero no consultamos Firestore en cada render: al iniciar, sincronizamos una vez y cacheamos en localStorage. Esto significa que el juego arranca sin depender de la red, y aun así las compras se reflejan correctamente entre dispositivos.
Integración con Stripe y Firebase
La monetización usa funciones serverless de Vercel: checkout.js crea una sesión Stripe, stripe-webhook.js escucha eventos y escribe entitlements a Firestore, verify-purchase.js valida antes de desbloquear contenido, y link-purchase.js vincula compras guest a cuentas autenticadas. Todo el flujo ocupa menos de 300 líneas de Node.js.
Qué haría diferente
Si empezara de cero, cambiaría algunas decisiones:
- Shippear mobile-first en lugar de retrofit
- Escribir el motor de historia con async loading por capítulo — reduciría el bundle inicial un 40%
- Añadir analytics desde el día uno para ver dónde abandonan los playtesters
- No construir el sistema de logros hasta tener 10 jugadores reales, porque la mitad de los logros necesitaron tuning tras playtest
Estadísticas de desarrollo
Números aproximados:
- Meses del concepto a v3.8: ~3
- Líneas de código: ~10.000
- Escenas narrativas: 63
- Horas de vida vertidas aquí: no preguntes
Construido en Irving, Texas. Alimentado por demasiado café y demasiada música ambient.
Posts individuales del devlog
- Por qué JavaScript Vanilla — Elecciones de stack tecnológico y trade-offs.
- Diseñando la Línea Temporal Sagrada — Cómo evolucionó la mecánica de rebobinado.
- Audio Procedural — Cero archivos de audio, variación infinita en tiempo de ejecución vía Web Audio API.
- Retratos ASCII — Por qué los retratos de personajes son texto.
- Integración Stripe y Firebase — Arquitectura de entitlements server-side.
- Lecciones de Three.js — Renderizado de combate 3D en el navegador.
- Filosofía de Monetización — Juego base gratis + epílogos pagados, por qué.