# TikXander Developers — documentación completa

> Fuente: https://developers.tikxander.com/docs — contrato de eventos v1, manifest, transportes, recetas.
> Pega este archivo entero en tu asistente de IA para darle todo el contexto de una vez.

---

# Quickstart — tu primer juego web

En 5 minutos tienes un juego que reacciona a eventos de TikTok Live, corriendo en tu
máquina con eventos simulados. No necesitas estar en vivo para desarrollar.

## 1. Crea el proyecto

```bash
npm create tikxander-game
```

Elige una plantilla (empieza con `vanilla` o `phaser`) y un nombre. Esto genera un proyecto
Vite con el SDK `@tikxander/live-events` ya cableado.

## 2. Instala el SDK desde el feed

El SDK vive en nuestro feed privado (Azure Artifacts). Copia el snippet `.npmrc` desde la card
**Credencial del feed** de tu dashboard y pégalo en la raíz del proyecto. Luego:

```bash
npm install
```

## 3. Corre en modo desarrollo con eventos simulados

En una terminal:

```bash
npm run dev
```

En otra, el simulador de eventos:

```bash
npm run mock
```

El mock inyecta `chat`, `gift`, `join`, `like`, `follow` y `live_status` en el mismo canal que
usarías en vivo. Tu juego no distingue un evento simulado de uno real (salvo el campo opcional
`sim: true` del envelope).

## 4. El bucle mínimo

```js
import { createClient } from '@tikxander/live-events';

const tx = createClient(); // detecta transporte del manifest (local-ws por defecto)

tx.on('init', ({ config }) => {
  // config llega RESUELTA (defaults del manifest ⊕ lo que guardó el streamer)
  console.log('comando de join:', config.comando_join);
});

tx.on('gift', ({ user, giftName, diamonds, count }) => {
  // intensidad = diamonds * count del streak
  spawnParticles(user.username, diamonds * count);
});

tx.on('chat', ({ user, message }) => {
  if (message.trim().toUpperCase() === 'JOIN') addPlayer(user);
});
```

## 5. Publica

Cuando tu juego esté listo, ve a **Mis Apps → Nueva app**, luego **Nueva versión** y sigue el
wizard de 4 pasos: sube tu `manifest.json`, tu build o repo, completa la ficha y envía a
revisión. Un admin la aprueba y queda disponible en Community.

> Regla de oro del modo demo: soporta `?txmode=demo`. Cuando la URL trae ese parámetro, tu juego
> NO conecta al agente: escucha `postMessage` del parent (`tx:init`/`tx:config`/`tx:event`) y
> responde `tx:ready`. El SDK ya lo maneja por ti si usas `createClient()`.

---

# Contrato de eventos v1

El corazón del SDK. Solo cambios aditivos: agregar campos opcionales está permitido; quitar,
renombrar o cambiar tipos exige un `v2` conviviendo con `v1`. Tipos de evento desconocidos se
IGNORAN (forward-compat), así que tu juego nunca se rompe por un evento nuevo.

## Envelope

Todo mensaje, por cualquier transporte, es un JSON con este sobre:

```json
{
  "v": 1,
  "id": "evt_01J9ZK3M7Q",
  "type": "chat",
  "ts": 1751990000000,
  "data": { }
}
```

| Campo | Tipo | Descripción |
|---|---|---|
| `v` | number | Versión del contrato. Siempre `1`. |
| `id` | string | Id único del evento (usa esto para deduplicar si reconectas). |
| `type` | string | Tipo de evento. Tipos desconocidos se IGNORAN. |
| `ts` | number | Epoch ms en que el agente capturó el evento. |
| `data` | object | Payload según tipo. |

## Objeto `user` (común a los eventos de viewer)

```json
{
  "userId": "6829385920134",
  "username": "pepito_gamer",
  "displayName": "Pepito 🎮",
  "avatarUrl": "https://p16-sign.tiktokcdn..."
}
```

`username` es el `uniqueId` de TikTok (estable, sin @). `avatarUrl` puede venir `null`.

## Eventos de live

### `chat` — mensaje en el chat

```json
{ "v": 1, "id": "evt_x1", "type": "chat", "ts": 1751990000000,
  "data": { "user": { "...": "..." }, "message": "JOIN" } }
```

### `gift` — regalo recibido

```json
{ "v": 1, "id": "evt_x2", "type": "gift", "ts": 1751990001000,
  "data": {
    "user": { "...": "..." },
    "giftId": 5655, "giftName": "Rose", "diamonds": 1,
    "count": 3, "streakEnd": true,
    "imageUrl": "https://..."
  } }
```

`count` es la cantidad acumulada del streak; `streakEnd: true` indica que el streak cerró. Para
gifts apilables, cuenta SOLO cuando `streakEnd` es `true`. En v1 los gifts llegan SIEMPRE con
`streakEnd: true` (el agente resuelve el streak antes de emitir). La intensidad de un gift es
`diamonds * count`.

### `join` — viewer entra al live

```json
{ "data": { "user": { "...": "..." } } }
```

### `like` — likes (agrupados)

```json
{ "data": { "user": { "...": "..." }, "count": 15, "totalLikes": 5230 } }
```

### `follow` — nuevo follower durante el live

```json
{ "data": { "user": { "...": "..." } } }
```

### `share` — viewer comparte el live

```json
{ "data": { "user": { "...": "..." } } }
```

### `live_status` — ciclo de vida del live

```json
{ "data": { "status": "started", "roomId": "7301234..." } }
```

`status` es `"started"` o `"ended"`.

## Mensajes de control (del sistema al juego)

### `init` — primer mensaje al conectar

```json
{ "v": 1, "type": "init", "ts": 1751990000000,
  "data": {
    "app": { "appId": "com.dev.batalla-clanes", "version": "1.2.0" },
    "live": { "active": true, "roomId": "7301234...", "host": "vureta" },
    "config": { "comando_join": "JOIN", "vidas_iniciales": 3, "gift_revive": 5655 }
  } }
```

`config` llega RESUELTA: los valores que el streamer guardó, con los defaults del manifest para
lo no configurado.

### `config_updated` — hot-reload de config

```json
{ "v": 1, "type": "config_updated", "ts": 1751990500000,
  "data": { "config": { "comando_join": "UNIRME", "vidas_iniciales": 5 } } }
```

Se emite cuando el streamer guarda cambios con el juego conectado. Llega la config COMPLETA (no
un diff).

### `ping` / `pong`

En WebSocket el servidor envía `{"v":1,"type":"ping"}` cada 30 s; responde `{"v":1,"type":"pong"}`.
El long-poll no lo necesita.

## Garantías y no-garantías

- **Orden**: en orden de captura por transporte local; por relay cloud se preserva dentro del cursor.
- **Entrega**: at-most-once en WebSocket (si te desconectas pierdes lo emitido mientras tanto);
  long-poll y relay tienen cursor con buffer corto (~60 s) para no perder ráfagas.
- **Sin persistencia**: el canal no es historial. Si necesitas acumular, acumula tú.
- **Rate**: sin throttling en v1. Un live grande puede emitir decenas de eventos/s; procesa
  barato o encola internamente.

## Soporte inicial

La captura actual soporta desde el día uno: `chat`, `gift`, `join`, `like`, `follow` y
`live_status`. Aún no se emiten `share` ni `like.data.totalLikes`; `user.avatarUrl` puede venir
`null`. Como los tipos ausentes simplemente no llegan y el cliente ignora tipos desconocidos,
agregar soporte después es 100% compatible.

---

# Manifest v1

El manifest es lo único que entregas a la plataforma (además de tu listing). Declara identidad,
plataforma, transporte, eventos que consumes y el schema de configuración que el streamer verá
renderizado en el portal. Se valida en el servidor al publicar; el wizard te muestra los mismos
errores EN VIVO mientras editas.

## Ejemplo completo

```json
{
  "manifestVersion": 1,
  "appId": "com.pepitodev.batalla-clanes",
  "name": "Batalla de Clanes",
  "version": "1.2.0",
  "platform": "gtasa",
  "transport": "local-poll",
  "events": ["chat", "gift", "join"],

  "listing": {
    "title": "Batalla de Clanes para GTA San Andreas",
    "shortDescription": "Tus viewers pelean en bandos escribiendo JOIN en el chat",
    "description": "Markdown largo con instrucciones de instalación…",
    "coverImage": "cover.png",
    "videos": ["https://www.youtube.com/watch?v=XXXX"],
    "downloadUrl": "https://github.com/pepitodev/batalla-clanes/releases/latest",
    "categoryIds": ["<id-de-categoria>"]
  },

  "config": [
    { "key": "comando_join", "type": "string", "default": "JOIN",
      "label": "Comando para unirse", "group": "basico" },
    { "key": "vidas_iniciales", "type": "number", "default": 3, "min": 1, "max": 10,
      "label": "Vidas iniciales", "group": "basico" },
    { "key": "equipos", "type": "select", "default": "2",
      "options": [ {"value": "2", "label": "2 equipos"}, {"value": "4", "label": "4 equipos"} ],
      "label": "Cantidad de equipos", "group": "basico" },
    { "key": "gift_revive", "type": "gift-picker", "default": 5655,
      "label": "Gift que revive al jugador", "group": "avanzado" },
    { "key": "modo_hardcore", "type": "boolean", "default": false,
      "label": "Modo hardcore", "group": "avanzado" }
  ]
}
```

## Campos raíz

| Campo | Req | Descripción |
|---|---|---|
| `manifestVersion` | sí | Siempre `1`. |
| `appId` | sí | Id único, inmutable entre versiones. `^[a-z0-9][a-z0-9.-]{2,99}$` (3–100 chars). |
| `name` | sí | Nombre corto interno. |
| `version` | sí | Semver `x.y.z`. Cada publicación sube versión (estrictamente creciente). |
| `platform` | sí | `web` \| `gta5` \| `gtasa` \| `roblox` \| `other`. |
| `transport` | sí | `local-ws` \| `local-poll` \| `cloud-relay`. Roblox obliga `cloud-relay`. |
| `events` | sí | Subconjunto no vacío de: `chat`, `gift`, `join`, `like`, `follow`, `share`, `live_status`. El canal solo entrega lo declarado. |
| `listing` | sí | Ficha pública (abajo). |
| `config` | — | Schema de configuración (abajo). Puede ser `[]`. |

## `listing` — la ficha en Community

| Campo | Req | Reglas |
|---|---|---|
| `title` | sí | ≤ 80 chars. |
| `shortDescription` | sí | ≤ 160 chars (card del catálogo). |
| `description` | sí | Markdown, ≤ 20k. Las instrucciones de instalación de un mod van aquí. |
| `coverImage` | sí | Nombre del archivo que subes en el wizard. 16:9, ≤ 2 MB. |
| `screenshots` | — | Hasta 6, mismas reglas que la carátula. |
| `videos` | — | Links de YouTube (embed). No hospedamos video. |
| `downloadUrl` | sí (mods externos) | Link EXTERNO de descarga. Con el flag `hostedUploads` puedes en cambio subir los archivos del mod. |
| `categoryIds` | sí | 1–3 categorías existentes y visibles. Se validan al publicar. |

## `source` — origen del código (solo `platform: "web"`)

Los juegos web community se hospedan con nosotros y quedan en
`https://communitygame.tikxander.com/<appId>/index.html`. El manifest declara el origen:

```json
"source": { "type": "upload" }
```

o, para importar desde un repositorio:

```json
"source": { "type": "github", "repo": "https://github.com/tu/repo", "branch": "main", "path": "dist" }
```

- Con `type: "upload"` subes un ZIP del build estático (requiere el flag `hostedUploads`).
- Con `type: "github"` clonamos el repo (`branch` + `path`). Si el repo es privado, el wizard te
  pide un token de SOLO lectura: se guarda cifrado, viaja fuera del manifest y puedes revocarlo
  tras la revisión.
- Sin el flag `hostedUploads`, `platform: "web"` exige `type: "github"`.

## `config` — schema de configuración

Cada campo se renderiza en el portal del streamer. Reglas por campo:

| Regla | Detalle |
|---|---|
| `key` | `^[a-z0-9_]+$`, único en el array. |
| `type` | `string` \| `number` \| `boolean` \| `select` \| `color` \| `gift-picker` \| `media-picker`. |
| `group` | `basico` o `avanzado` (organiza la UI del streamer). |
| `default` | OBLIGATORIO en TODOS los campos. |
| `select` | requiere `options: [{value, label}]`; el `default` debe estar entre los `value`. |
| `number` | opcional `min`/`max` (con slider), `step`, `integer: true` (fuerza enteros) y `unit` (sufijo mostrado, ej. `"seg"`, `"%"`). El `default` debe caer en el rango. |
| `gift-picker` | el streamer **elige del catálogo real** (imagen + diamantes), no tipea el id. Guarda el `giftId` de TikTok (estable entre regiones). Con `"multiple": true` guarda un `number[]` (varios regalos → mismo efecto); su `default` es un array. |
| `visibleIf` | muestra el campo condicionalmente: `{ "field": "<otra_key>", "equals": <valor> }` o `{ "field": "...", "oneOf": [<valores>] }`. Ej.: mostrar `mult_duracion_seg` solo si `mult_es_permanente === false`. |
| Aviso | si dos `gift-picker` comparten el mismo `giftId`, el portal muestra una advertencia (el mismo regalo dispararía varios efectos). |
| Evolución | un `key` existente NO puede cambiar de `type` entre versiones (usa un key nuevo). |

## Códigos de validación

El servidor devuelve un array `[{field, code, message}]` y el wizard los pinta EN VIVO sobre los
campos. Algunos códigos: `manifest_version_invalid`, `app_id_invalid`, `app_id_immutable`,
`name_required`, `version_invalid`, `semver_not_increasing`, `platform_invalid`,
`transport_invalid`, `roblox_requires_cloud_relay`, `events_invalid`, `listing_title_invalid`,
`listing_short_invalid`, `listing_description_invalid`, `cover_required`, `too_many_screenshots`,
`video_url_invalid`, `categories_count_invalid`, `category_not_found`, `source_type_invalid`,
`source_github_required`, `bundle_required`, `github_source_incomplete`, `config_key_invalid`,
`config_key_duplicate`, `config_group_invalid`, `config_default_required`,
`config_options_required`, `config_default_not_in_options`, `config_default_type_mismatch`,
`config_default_out_of_range`, `config_type_changed`.

---

# Transportes — un contrato, tres caminos

El evento JSON es idéntico en los tres transportes. El manifest declara cuál usa tu app
(`transport`). El SDK elige el correcto por ti.

## 1. `local-ws` — WebSocket local (web, GTA V, escritorio)

```
ws://127.0.0.1:8767/events/v1?app=<appId>
```

- El servidor es el agente local del streamer.
- Handshake: el primer mensaje del servidor es `init`; luego el stream de eventos declarados en
  el manifest. `ping`/`pong` cada 30 s.
- **Permisos**: la primera conexión de un `appId` desconocido dispara un prompt en el agente
  ("Este juego quiere recibir: chat, gifts, joins — ¿Permitir?"), con opción de recordar.
- Cliente de referencia: `@tikxander/live-events` (conectar, reconectar con backoff, deduplicar
  por `id`, `on('chat', cb)`).

## 2. `local-poll` — HTTP long-poll local (GTA San Andreas y engines viejos)

Para entornos sin WebSocket viable (scripts CLEO/Lua):

```
GET http://127.0.0.1:8767/events/v1/poll?app=<appId>&cursor=<c>&wait=25
→ 200 { "cursor": "c124", "events": [ {…}, {…} ] }
```

- `cursor` opaco; la primera llamada sin cursor devuelve `init` + el cursor actual.
- `wait`: long-poll hasta N segundos (máx 25); responde antes si hay eventos.
- Buffer de ~60 s por app: una ráfaga de gifts no se pierde entre polls.
- Sirve para cualquier lenguaje con HTTP; `curl` basta para probar.

## 3. `cloud-relay` — HTTPS polling cloud (Roblox, obligatorio)

Los juegos Roblox corren en servidores de Roblox y no ven el `127.0.0.1` del streamer. Su camino
es un relay en agent-controller:

```
GET https://agentcontroller.tikxander.com/relay/v1/events?cursor=<c>
Authorization: Bearer <relay-token del streamer para esa app>
→ 200 { "cursor": "c88", "events": [ {…} ] }
```

- El `relay-token` se genera en la ficha del juego (por streamer + app, revocable) y el streamer
  lo pega en la config de su experiencia Roblox.
- Cola FIFO consumida por HttpService, con cursor + buffer corto.

## Matriz plataforma → transporte

| Plataforma | Transporte | SDK | Por qué |
|---|---|---|---|
| Juego web (HTML) | `local-ws` | `@tikxander/live-events` (npm) | Corre en el browser/OBS, localhost OK. |
| Mod GTA V | `local-ws` | SDK C# (ScriptHookVDotNet) | .NET tiene WebSocket nativo. |
| Mod GTA SA | `local-poll` | helper CLEO/Lua | Sin WebSocket viable. |
| Roblox | `cloud-relay` | ModuleScript Lua | Los servers de Roblox no ven localhost. |
| Otro (Python, Unity…) | `local-ws` o `local-poll` | protocolo crudo | Cualquier lenguaje con HTTP se integra. |

## Simulador

El panel de simulación (en este portal y en la pestaña Desarrollador del agente) inyecta eventos
falsos en el MISMO canal: tu juego no distingue (salvo el campo opcional `sim: true`). Presets:
chat suelto, gift x1, streak de gifts, ráfaga de joins, live start/end, caos aleatorio N
eventos/s.

---

# Errores comunes — código, qué significa y cómo se arregla

Cada error del sistema es un código snake_case estable. Aquí están los más frecuentes con el fix
exacto.

## Publicación (validación del manifest)

| Código | Significa | Fix |
|---|---|---|
| `manifest_invalid` | El JSON no parsea o no cumple el esquema raíz. | Valida el JSON; revisa comas y comillas. |
| `app_id_invalid` | El `appId` no cumple el formato. | Usa `^[a-z0-9][a-z0-9.-]{2,99}$` (minúsculas, 3–100 chars). |
| `app_id_immutable` | El `appId` del manifest difiere del de la app. | El `appId` es inmutable; no lo cambies entre versiones. |
| `semver_not_increasing` | La versión es ≤ a la última publicada. | Sube la versión (semver estrictamente creciente). |
| `roblox_requires_cloud_relay` | `platform: roblox` sin `cloud-relay`. | Pon `transport: "cloud-relay"`. |
| `config_default_required` | Un campo de config no tiene `default`. | Agrega `default` a TODOS los campos de config. |
| `config_type_changed` | Un `key` cambió de `type` entre versiones. | Usa un `key` nuevo en vez de cambiar el tipo del existente. |
| `cover_required` | Falta la carátula o no coincide el nombre. | Sube la carátula; `coverImage` debe ser el nombre del archivo subido. |
| `categories_count_invalid` | Fuera del rango 1–3 categorías. | Elige entre 1 y 3 categorías. |
| `source_github_required` | `web` sin `hostedUploads` y sin `source.type: github`. | Usa origen GitHub, o pide el flag de subidas hospedadas. |
| `github_source_incomplete` | Faltan `repo`/`branch`/`path`. | Completa los tres campos del origen GitHub. |

## En vivo (conexión y permisos)

| Código | Significa | Fix |
|---|---|---|
| `app_not_allowed` | El streamer no aprobó tu app en el agente. | Pídele que acepte el prompt de permisos (o que active el modo dev para probar). |
| `app_not_published` | La app aún no está publicada/aprobada. | Espera la aprobación, o usa el modo dev del agente para desarrollo. |

## Desarrollo

- **No llegan eventos con `npm run dev`**: asegúrate de correr también `npm run mock` en otra
  terminal. Sin el mock (o sin estar en vivo con el agente) no hay fuente de eventos.
- **El juego no aparece en el preview demo**: revisa que respondas `tx:ready` a los mensajes
  `postMessage` del parent cuando la URL trae `?txmode=demo`. El SDK lo hace por ti con
  `createClient()`.
- **Los gifts se disparan de más**: cuenta el streak SOLO cuando `streakEnd` es `true`. En v1
  siempre llega `true`, pero escríbelo defensivamente para v2.

---

# Construye con IA — el prompt inicial

La forma más rápida de construir en TikXander es pegarle a tu asistente (Claude Code, Cursor,
Codex…) el contexto completo del contrato y este prompt inicial. Todo el contrato cabe en un
archivo: [`llms-full.txt`](/llms-full.txt).

## Los 3 pasos

1. `npm create tikxander-game` — elige la plantilla (`vanilla` o `phaser` para empezar).
2. Pega el prompt de abajo en tu asistente (y el contenido de `llms-full.txt`).
3. `npm run dev` + `npm run mock` — desarrolla con eventos simulados.

## Prompt inicial

```text
Eres mi copiloto para construir un juego de TikTok Live sobre el SDK @tikxander/live-events.
Contexto completo del contrato: pega el contenido de https://developers.tikxander.com/llms-full.txt.

Objetivo: construir un juego web (platform "web", transport "local-ws") que reaccione a los
eventos v1: chat, gift, join, like, follow, share, live_status. Cada gift trae diamantes; usa
la intensidad (diamantes × cantidad del streak) para escalar el efecto.

Reglas:
- Usa el SDK: import { createClient } from '@tikxander/live-events'.
- No inventes eventos ni campos: cíñete al contrato de eventos v1 de llms-full.txt.
- El manifest.json debe validar contra el contrato (manifestVersion 1, appId
  ^[a-z0-9][a-z0-9.-]{2,99}$, semver, listing completo, config con default y group en cada campo).
- Soporta modo demo (?txmode=demo) escuchando postMessage del parent y respondiendo tx:ready.
```

## Por qué funciona

Nuestra documentación está escrita para que un LLM la consuma: cada evento con su JSON copiable,
cada página autocontenida. Dado solo `llms-full.txt`, un asistente puede armar un manifest válido
y un game loop correcto al primer intento. Si algo falla, el wizard de publicación te muestra el
error exacto por campo — pégaselo a tu IA y lo corrige.

---

# Recetas — juegos elegantes por motor

Recetas copiables para los 5 motores de las plantillas. Todas asumen que ya tienes el cliente
conectado:

```js
import { createClient } from '@tikxander/live-events';
const tx = createClient();
```

## Elige tu motor

| Motor | 2D/3D | Física | Curva | Ideal para |
|---|---|---|---|---|
| **Vanilla** (Canvas/DOM) | 2D | manual | baja | Contadores, metas, textos flotantes, listas/ranking. |
| **Phaser** | 2D | Arcade/Matter | media | Bolitas-avatar, arcade, colisiones, partículas. |
| **PixiJS** | 2D | ninguna (render) | media | Miles de sprites, efectos visuales pesados. |
| **Babylon.js** | 3D | Havok/cannon | alta | Escenas 3D completas, cámaras, luces. |
| **Three.js** | 3D | manual/cannon | alta | 3D a medida, shaders, arte generativo. |

Regla general: si es UI/2D simple, **vanilla** o **Phaser**. Si necesitas muchos sprites,
**PixiJS**. Si es 3D, **Babylon** (más batería incluida) o **Three** (más control).

## Receta 1 — spawn de avatar al join

Un viewer entra y aparece su avatar.

**Vanilla (DOM):**

```js
tx.on('join', ({ user }) => {
  const el = document.createElement('img');
  el.src = user.avatarUrl ?? '/anon.png';
  el.className = 'avatar';
  el.style.left = Math.random() * innerWidth + 'px';
  document.body.appendChild(el);
  setTimeout(() => el.remove(), 6000);
});
```

**Phaser:**

```js
tx.on('join', ({ user }) => {
  const x = Phaser.Math.Between(50, scene.scale.width - 50);
  const ball = scene.matter.add.image(x, 0, 'avatar').setCircle(24).setBounce(0.8);
  scene.load.image(user.userId, user.avatarUrl); // precarga real fuera del handler
});
```

## Receta 2 — reacción a gift con intensidad por diamantes

La clave es la intensidad `diamonds * count`.

```js
tx.on('gift', ({ user, diamonds, count }) => {
  const intensity = diamonds * count;      // 1 rosa = 1; 100 rosas = 100; un gift caro escala
  const scale = Math.min(1 + intensity / 50, 6);
  burstParticles({ amount: Math.min(intensity, 300), scale, who: user.username });
});
```

**PixiJS (partículas):**

```js
function burstParticles({ amount, scale }) {
  for (let i = 0; i < amount; i++) {
    const p = new PIXI.Sprite(spark);
    p.position.set(app.screen.width / 2, app.screen.height / 2);
    p.scale.set(0.2 * scale);
    const a = Math.random() * Math.PI * 2, v = 2 + Math.random() * 4 * scale;
    p.vx = Math.cos(a) * v; p.vy = Math.sin(a) * v;
    stage.addChild(p); particles.push(p);
  }
}
```

## Receta 3 — texto flotante de chat

```js
tx.on('chat', ({ user, message }) => {
  floatText(`${user.displayName}: ${message}`, {
    x: Math.random() * innerWidth, y: innerHeight - 40, ttl: 4000,
  });
});
```

## Receta 4 — ranking por gifts (acumula tú, el canal no persiste)

```js
const score = new Map();
tx.on('gift', ({ user, diamonds, count }) => {
  score.set(user.username, (score.get(user.username) ?? 0) + diamonds * count);
  renderLeaderboard([...score].sort((a, b) => b[1] - a[1]).slice(0, 10));
});
```

## Receta 5 — 3D: aparecer un cubo por follow (Three.js)

```js
tx.on('follow', ({ user }) => {
  const cube = new THREE.Mesh(geo, mat.clone());
  cube.position.set((Math.random() - 0.5) * 8, 4, 0);
  scene.add(cube);
  bodies.push({ cube, vy: 0 }); // integra gravedad en tu loop
});
```

**Babylon.js (con física Havok):**

```js
tx.on('follow', () => {
  const sphere = BABYLON.MeshBuilder.CreateSphere('s', { diameter: 1 }, scene);
  sphere.position.y = 6;
  new BABYLON.PhysicsAggregate(sphere, BABYLON.PhysicsShapeType.SPHERE, { mass: 1 }, scene);
});
```

## Buenas prácticas

- **Cuenta el streak una vez**: procesa el gift cuando `streakEnd === true` (en v1 siempre lo es).
- **Precarga assets**: no cargues texturas dentro del handler de un evento; cachéalas por `userId`.
- **Procesa barato**: un live grande emite decenas de eventos/s. Encola y consume en tu game loop.
- **Modo demo**: con `?txmode=demo` el SDK simula eventos; úsalo para la preview de la ficha.

---

# Estructura del proyecto y forma de trabajar

Cada proyecto que scaffoldeás con `create-tikxander-game` viene con **reglas claras
desde el minuto cero** para que tu asistente de IA (cualquier modelo) trabaje
ordenado y no se vaya por las ramas. Estas reglas son parte del proyecto: viven en
`AGENTS.md` y en `.ai/rules/`, y **el `build` las verifica**.

## Reglas de estructura

- **Ningún archivo supera 500 líneas** (ideal < 300). Si un archivo crece, se
  divide. El `build` corre `scripts/check-structure.mjs` (zero-dep) y **falla** si
  te pasás — así un archivo gigante nunca llega a publicarse.
- **Un archivo, una responsabilidad**, con nombre descriptivo.
- El código de `src/` se organiza en **carpetas de concern** con prefijo `_`:

| Carpeta | Qué va acá |
|---------|-----------|
| `src/_models/` | Tipos e interfaces (estado, payloads, config) |
| `src/_enum/` | Enums / uniones de estado (fases, tipos de item) |
| `src/_const/` | Constantes y defaults (colores, umbrales, dimensiones) |
| `src/_services/` | Lógica pura sin DOM (puntajes, ranking, física, IO del SDK) |
| `src/_hooks/` | Helpers de estado/animación reutilizables |
| `src/_components/` | Piezas de render / DOM reutilizables |

En un **juego**, `src/main.ts` hace el bootstrap del SDK y `src/tikxander-bridge.ts`
es el punto de entrada de tu lógica (delegá en `_entities/` y `_systems/` si crece).
En un **widget**, tu lógica vive en las 3 funciones de `createWidget()` en
`src/main.ts` (o extraídas a `src/_logic/`), y los estilos en `src/style.css`.

> `_hooks`/`_components` son idiomáticos de frameworks con hooks; en un juego canvas
> mapean a "helpers de estado/animación" y "piezas de render". Mantené los nombres
> para tener **un solo modelo mental** ordenado.

## Forma de trabajar: planificá antes de codear (SDD ligero)

Para una feature **no trivial** (toca varias piezas, hay decisiones de diseño, o no
cabe en una frase), tu proyecto trae una carpeta `_specs/` con una plantilla:

1. Copiá `_specs/_plantilla/` a `_specs/NNN-<slug>/`.
2. Llená los 4 archivos: **context** (por qué) → **spec** (qué técnico) → **plan**
   (pasos) → **task** (Definition of Done medible).
3. Mostrale el plan a tu humano y **esperá el OK** antes de implementar.

Un cambio trivial (un color, un texto) no necesita spec: hacelo directo.

### Preguntá hasta tener todo claro

La regla más importante para que la IA no invente: antes de escribir código,
**reformulá lo que entendiste y hacé todas tus preguntas juntas** (mecánica, qué
evento dispara qué, defaults, qué configura el streamer, tamaño del overlay). Si
algo no está claro, **preguntá — no asumas**.

## Model-agnostic: una fuente de verdad, adaptadores finos

Las reglas viven una sola vez en **`AGENTS.md`** + `.ai/rules/`. Los archivos de cada
asistente son adaptadores mínimos que apuntan ahí, así funciona con **cualquier IA**
sin duplicar contenido:

| Archivo | Herramienta |
|---------|-------------|
| `AGENTS.md` | Fuente de verdad (Codex, Grok, Jules, Zed…) |
| `CLAUDE.md` | Claude Code (`@import` a `AGENTS.md` + `.ai/rules/`) + hook de 500 líneas |
| `.cursor/rules/tikxander.mdc` | Cursor / Windsurf |
| `.github/copilot-instructions.md` | GitHub Copilot |
| `GEMINI.md` | Gemini CLI |

## Qué verifica el `build`

```bash
npm run build   # node scripts/check-structure.mjs && tsc --noEmit && vite build
```

- `check-structure.mjs` → ningún archivo > 500 líneas; carpetas de `src/` válidas.
- `tsc --noEmit` → tipos.
- `vite build` → bundle.

La publicación en el portal corre el `build`, así que estas reglas se respetan
**antes** de que tu juego/widget salga al catálogo.

---
