//alexis.dev
Volver al blog
7 min de lectura

Formosa Virtual es un pueblo comercial virtual e isométrico en pixel art inspirado en el centro de Formosa, Argentina. Caminas con tu avatar por la Plaza San Martín, la Av. 25 de Mayo y la Costanera Vuelta Fermoza, entras a las tiendas locales, hablas con los NPCs, armas tu carrito y haces el pedido por WhatsApp. Con multijugador en tiempo real y chat moderado con IA.

La Plaza San Martín en el juego: la fuente, el monumento, la calesita y el puesto de panchos

La Plaza San Martín en el juego — con mi avatar frente al afiche de "Se busca" del Pomberito.

En este post cuento las decisiones técnicas que más me costaron — y las que más disfruté.

La idea: e-commerce disfrazado de juego

Los comercios de Formosa ya venden por WhatsApp; es el canal real de venta local. Entonces, en vez de construir "otra tienda online" que nadie iba a usar, la pregunta fue: ¿y si comprar fuera caminar por tu propia ciudad? El juego es la interfaz; el checkout es un mensaje de WhatsApp ya redactado con tu pedido.

Eso definió el norte técnico del MVP: cero fricción y cero infraestructura. Sin registro, sin pasarela de pago, sin base de datos.

El punto de partida

No empecé de cero: partí del template shopify-game de v0 y terminó irreconocible. Reemplacé Shopify por un catálogo propio + WhatsApp, traduje todo al español rioplatense y rediseñé el mundo completo para replicar el corredor urbano real de Formosa. El stack quedó en Next.js 16, React 19 y Tailwind 4.

Todo el juego lo construí en colaboración con Fable 5, el modelo de Anthropic, apenas se lanzó su primera versión. Además de un proyecto para mi ciudad, fue un experimento personal: ¿hasta dónde se puede llegar construyendo un motor de juego desde cero con IA como par de programación? La respuesta, spoiler: mucho más lejos de lo que esperaba — pero solo si tú aportas el criterio, el contexto y las referencias correctas.

Empezar desde un template no le quita mérito al proyecto — lo importante es lo que construyes encima. En mi caso, el motor gráfico completo, el mundo, el multijugador y el checkout son propios.

Un motor isométrico sin librerías de juegos

La decisión más radical: nada de Phaser, PixiJS ni motores de juego. Todo es Canvas 2D crudo, unas 22.000 líneas entre el dibujo procedural (iso.ts, ~15.000 líneas), el mundo y el game loop.

Proyección

La base de cualquier mundo isométrico es convertir coordenadas de grilla a pantalla. Con tiles de 64×32:

export const TILE_W = 64;
export const TILE_H = 32;
 
// Convierte coordenadas de grilla (continuas) al centro de la celda en pantalla.
export function worldToScreen(gx: number, gy: number): Vec2 {
  return {
    x: (gx - gy) * (TILE_W / 2),
    y: (gx + gy) * (TILE_H / 2),
  };
}

Dos líneas de matemática, pero de ellas cuelga todo lo demás.

El problema que no esperaba: el orden de dibujo

En isométrico, "qué se dibuja encima de qué" no es trivial. La heurística clásica (ordenar por x + y) se rompe apenas tienes edificios que ocupan varios tiles: la esquina frontal de una casa 2×2 siempre da un valor enorme, y el personaje parado junto a la puerta quedaba dibujado detrás de la pared.

La solución fue un orden topológico por "footprint": cada entidad declara el rango de tiles que ocupa, y una función behind(a, b) decide caso por caso:

  • Si comparten filas en un eje, la profundidad la decide el otro eje.
  • Si se solapan en ambos ejes (el jugador pegado a la pared de una casa), decide la profundidad del ancla — los pies del personaje — no el borde frontal del edificio.
  • Solo cuando están separados en diagonal se usa la heurística clásica del borde frontal.

Con eso, un insertion sort estable por frame resuelve todas las oclusiones sin casos especiales por tipo de entidad.

Sprites horneados, nítidos a cualquier zoom

Redibujar cientos de props procedurales (árboles, faroles, bancos) con operaciones de path en cada frame no escala. El motor hornea cada prop una sola vez a un canvas offscreen y después lo copia con drawImage.

El detalle fino está en el zoom: si horneas a 1x y el jugador acerca la cámara, el navegador re-escala el bitmap y se ve borroso. Por eso cada sprite se hornea a la escala de dispositivo (el bucket entero de dpr × zoom, topado en 3x) y se re-hornea solo cuando cambia el bucket — pocas veces, no por frame. Una cache LRU de 800 sprites mantiene la memoria a raya, incluyendo las ~96 variantes de flora del pasto.

Reconstruir una ciudad real en 76×30 tiles

El mundo es un corredor este-oeste que replica el centro verdadero de Formosa:

  • Plaza San Martín al oeste: el monumento ecuestre, la fuente Irupé, el lago con puentecito, el skatepark y sus 8 senderos.
  • Av. 25 de Mayo: doble calzada con plazoleta central caminable, el Reloj Histórico, frentes comerciales continuos y la Catedral.
  • Costanera Vuelta Fermoza al este: balaustrada blanca, palmeras, la Casa de Gobierno con su bandera animada y el muelle-mirador sobre el río Paraguay.

Y aquí está lo que hizo la diferencia: no trabajamos de memoria. Le dimos al modelo fotos aéreas de Formosa tomadas con dron como referencia, y de ahí salieron las proporciones de la plaza, el trazado de la doble calzada con su plazoleta central y la curva de la costanera sobre el río. Ver la ciudad desde arriba es, literalmente, la misma perspectiva que un mundo isométrico — las fotos funcionaron casi como planos.

La zona de la Costanera Vuelta Fermoza y el río Paraguay, con los lapachos en flor

La zona de la costanera y el río Paraguay, con los lapachos en flor. Foto: Gastón Scheinner.

Las fotos las aportó mi amigo Gastón Scheinner, que las tomó con su dron. Gracias, Gastón — sin ese material, el mapa habría sido una aproximación genérica en vez de un lugar que un formoseño reconoce al primer vistazo.

El muelle y las embarcaciones sobre el río Paraguay

El muelle sobre el río Paraguay — en el juego es el muelle-mirador del extremo este del mapa.

La avenida de la costanera con sus palmeras

La avenida de la costanera con sus palmeras y veredas de piedra, tal cual está replicada en el mundo del juego.

Todo es dibujo procedural: no hay spritesheets ni assets de imagen. Cada landmark es código que dibuja paths sobre el canvas. Es más lento de producir que importar tiles prefabricados, pero el resultado es único — y agregar una tienda jugable nueva son tres toques: su ficha, sus productos y su posición en el mapa.

Sin base de datos, a propósito

El catálogo vive en un archivo TypeScript estático, y el checkout es una Server Action que arma el mensaje del pedido y devuelve un link wa.me. Nada que administrar, nada que se caiga.

No es una limitación, es una decisión de MVP con la puerta abierta: para migrar a base de datos solo hay que reemplazar un módulo por una consulta que devuelva el mismo tipo Category[]. La interfaz ya está definida; la implementación es intercambiable.

Multijugador y moderación con IA

Dos features que suenan grandes pero se resolvieron con poco código propio:

  • Multijugador: Liveblocks presence sincroniza posición y apariencia de los avatares, con sharding automático de salas cuando una se llena. Sin la API key, el juego degrada limpio a single-player.
  • Chat con IA: los mensajes y nombres pasan por un endpoint de moderación con IA. Si el servicio no está disponible, la moderación falla abierta — se mantiene solo el rate limit — porque en un juego casual es peor bloquear el chat de todos que dejar pasar un mensaje.

Ese patrón se repite en todo el proyecto: cada dependencia externa es opcional y degrada con gracia.

Lo que sigue

Peatones NPC, más landmarks (el Predio Ferial, el puerto, la Iglesia Don Bosco), catálogo en base de datos con panel para que los comercios carguen sus productos, y Mercado Pago.

Si eres de Formosa — o simplemente te da curiosidad caminar por una ciudad argentina en pixel art — el juego está online. Y si estás construyendo algo con Canvas 2D y te trabaste con el depth-sort isométrico, escríbeme: perdí suficientes horas ahí como para ahorrarte unas cuantas.