[
 {
  "name": "status",
  "category": "meta",
  "status": "verified",
  "mechanism": "bloque del mundo + carril rápido + tabla de reservas",
  "latency": "Cálculo local",
  "signature": "status() -> 'dict'",
  "doc": "Estado de la conexión: pid, publicación del mundo (periodo, tiempo de recopilación), contadores del carril rápido."
 },
 {
  "name": "snapshot",
  "category": "observe",
  "status": "verified",
  "mechanism": "Bloque del mundo W3P Local\\War3World_<pid> (el runtime lo publica cada 50 ms, seqlock)",
  "latency": "Snapshot publicado",
  "signature": "snapshot(max_age: 'float' = 0.05)",
  "doc": "Estado completo de todo el mapa (WorldState): .units .players .items .clock .me; las llamadas repetidas dentro de max_age segundos devuelven la misma copia.\n⚠ Los trabajadores que están dentro de una mina de oro no aparecen; por defecto se ve todo el mapa (con el modelo lockstep, el cliente local lo tiene todo); solo Game(fair=True) filtra según la visión."
 },
 {
  "name": "last_seen",
  "category": "observe",
  "status": "verified",
  "mechanism": "visibleTo de la instantánea push (en cada refresco de la instantánea se registran las unidades enemigas / creeps visibles)",
  "latency": "Snapshot publicado",
  "signature": "last_seen(owner: 'str | int' = 'enemy', max_age: 'float | None' = None) -> 'list'",
  "doc": "Unidades enemigas (o 'creep' para creeps, o un número de jugador) vistas por última vez: [(la unidad tal como era entonces, reloj de juego en ese momento, segundos transcurridos)], las más recientes primero.\nSi la ves morir, se borra de la lista. Tanto en modo justo como en modo normal se registra según \"lo que vemos en este momento\": es el mapa que un jugador lleva en la cabeza:\nel ejército que ha explorado, dónde vio por última vez al héroe rival, cuándo abrió el rival su expansión. max_age: solo las de los últimos tantos segundos de juego."
 },
 {
  "name": "map",
  "category": "observe",
  "status": "verified",
  "mechanism": "Bloque de mapa W3P Local\\War3Map_<pid> (el runtime lo calcula por lotes al empezar la partida; IsTerrainPathable para caminar / construir)",
  "latency": "Snapshot publicado",
  "signature": "map()",
  "doc": "Tabla de terreno de esta partida, MapInfo: .walkable(x,y) .buildable(x,y) .at(x,y) .bounds (área jugable) .starts (puntos de inicio) .cells (bit0 no transitable, bit1 no edificable).\nTarda unos segundos en calcularse al empezar la partida; hasta entonces devuelve None. Los árboles no están incluidos (usa trees())."
 },
 {
  "name": "me",
  "category": "observe",
  "status": "verified",
  "mechanism": "cabecera del bloque del mundo",
  "latency": "Snapshot publicado",
  "signature": "me() -> 'int | None'",
  "doc": "Qué número de jugador soy (0~11)."
 },
 {
  "name": "resources",
  "category": "observe",
  "status": "verified",
  "mechanism": "bloque del mundo players[16]",
  "latency": "Snapshot publicado",
  "signature": "resources(player: 'int | None' = None) -> 'dict | None'",
  "doc": "{'gold','lumber','food_used','food_cap','gold_gathered','lumber_gathered'}; player es nuestro jugador por defecto; se puede leer el de cualquier jugador.\nSi no se puede leer devuelve None; no lo trates como 0."
 },
 {
  "name": "players",
  "category": "observe",
  "status": "verified",
  "mechanism": "bloque del mundo players[16]",
  "latency": "Snapshot publicado",
  "signature": "players() -> 'list'",
  "doc": "Los 16 slots de jugador: Player(id, gold, lumber, food_used, food_cap, gold_gathered, lumber_gathered, race, known)."
 },
 {
  "name": "units",
  "category": "observe",
  "status": "verified",
  "mechanism": "bloque del mundo units[]",
  "latency": "Snapshot publicado",
  "signature": "units(owner: 'str | int' = 'all', types=None, alive: 'bool' = True) -> 'list'",
  "doc": "Filtra unidades por dueño / tipo. owner: 'me' / 'enemy' / 'creep' / 'all' / número de jugador. types: conjunto de códigos de cuatro caracteres."
 },
 {
  "name": "unit",
  "category": "observe",
  "status": "verified",
  "mechanism": "bloque del mundo by_handle",
  "latency": "Snapshot publicado",
  "signature": "unit(handle) -> 'object | None'",
  "doc": "Busca una unidad por su par de handles (lo, hi) (el objetivo de la orden, el objetivo de tarea y los eventos dan pares de handles)."
 },
 {
  "name": "is_building",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea + units.json (spd==0 = edificio)",
  "latency": "Snapshot publicado",
  "signature": "is_building(u) -> 'bool'",
  "doc": "Si es un edificio (torres incluidas). Se decide por velocidad de movimiento 0 en la tabla de unidades; la huella del edificio principal de los No-muertos es 0, así que no lo decidas por la huella."
 },
 {
  "name": "my_workers",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea push",
  "latency": "Snapshot publicado",
  "signature": "my_workers() -> 'list'",
  "doc": "Nuestros trabajadores (campesino / peón / acólito / fuego fatuo)."
 },
 {
  "name": "idle_workers",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea push (slot de orden + slot de tarea)",
  "latency": "Snapshot publicado",
  "signature": "idle_workers() -> 'list'",
  "doc": "Trabajadores sin nada que hacer: sin orden y sin tarea (no cuentan los que acabas de mandar a trabajar en este tick).\n⚠ Volver a dar la orden de recolectar a un trabajador que tiene tarea interrumpe el ciclo de recolección (los ingresos caen a cero)."
 },
 {
  "name": "my_heroes",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea push",
  "latency": "Snapshot publicado",
  "signature": "my_heroes() -> 'list'",
  "doc": "Nuestros héroes vivos (los muertos están en la lista de resurrección del altar; ver revive)."
 },
 {
  "name": "my_army",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea push + units.json",
  "latency": "Snapshot publicado",
  "signature": "my_army() -> 'list'",
  "doc": "Nuestras unidades de combate: ni trabajadores ni edificios."
 },
 {
  "name": "my_buildings",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea push",
  "latency": "Snapshot publicado",
  "signature": "my_buildings(types=None) -> 'list'",
  "doc": "Nuestros edificios (torres y cimientos en construcción incluidos); con types puedes pedir solo algunos tipos, p. ej. {'hbar'}."
 },
 {
  "name": "is_constructing",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea push (orden = código de cuatro caracteres de un edificio, u orden de construir / reparar)",
  "latency": "Snapshot publicado",
  "signature": "is_constructing(worker) -> 'bool'",
  "doc": "Si este trabajador está construyendo (o va de camino a construir / está ayudando a reparar; incluye lo que acabas de ordenar en este tick). Sáltalo al elegir constructor; si no, los cimientos anteriores se quedarán parados."
 },
 {
  "name": "under_construction",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea push (la vida de los cimientos sube desde muy poco hasta el máximo)",
  "latency": "Snapshot publicado",
  "signature": "under_construction(building) -> 'bool'",
  "doc": "Este edificio aún no está terminado (vida no llena). ⚠ Un edificio dañado tampoco tiene la vida llena: basta para la apertura, pero una vez empiezan los combates hay que tener en cuenta también el tiempo."
 },
 {
  "name": "gold_mines",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea push (ngol/egol/ugol)",
  "latency": "Snapshot publicado",
  "signature": "gold_mines() -> 'list'",
  "doc": "Las minas de oro del mapa. ⚠ La mina de oro enredada de los Elfos de la noche y la mina neutral son dos unidades en las mismas coordenadas; manda a recolectar a la tuya."
 },
 {
  "name": "enemies",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea push",
  "latency": "Snapshot publicado",
  "signature": "enemies(fighters_only: 'bool' = False) -> 'list'",
  "doc": "Unidades de los jugadores enemigos (sin creeps). fighters_only: quita trabajadores y edificios."
 },
 {
  "name": "creeps",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea push (owner 12 = neutral hostil)",
  "latency": "Snapshot publicado",
  "signature": "creeps() -> 'list'",
  "doc": "Creeps (neutral hostil). ⚠ De noche la visión se reduce; cuando un campamento lejano queda en la niebla de guerra, los comandos dirigidos a él se rechazan (código de motivo 1001)."
 },
 {
  "name": "nearest",
  "category": "meta",
  "status": "verified",
  "mechanism": "cálculo puro",
  "latency": "Cálculo local",
  "signature": "nearest(candidates, to)",
  "doc": "El candidato más cercano a to (una unidad o (x,y)); sin candidatos devuelve None."
 },
 {
  "name": "life_mana",
  "category": "observe",
  "status": "verified",
  "mechanism": "unidad del bloque del mundo hp/hpMax/mana/manaMax",
  "latency": "Snapshot publicado",
  "signature": "life_mana(u) -> 'dict | None'",
  "doc": "{'hp','hp_max','mana','mana_max'} (float, valores en bruto del motor). Para u basta con la unidad obtenida de la instantánea (se sustituye por la copia más reciente)."
 },
 {
  "name": "hero_info",
  "category": "observe",
  "status": "verified",
  "mechanism": "unidad del bloque del mundo level/xp/skillPoints",
  "latency": "Snapshot publicado",
  "signature": "hero_info(hero) -> 'dict | None'",
  "doc": "{'level','xp','skill_points'}."
 },
 {
  "name": "abilities",
  "category": "observe",
  "status": "verified",
  "mechanism": "detalle del bloque del mundo: habilidades (código / nivel / flags / enfriamiento restante)",
  "latency": "Snapshot publicado",
  "signature": "abilities(u) -> 'list'",
  "doc": "[{code, level, cooldown, flags}]; los buffs están en buffs(u). Solo las unidades \"con detalle\" lo tienen (héroes > unidades de jugador > creeps, hasta 256)."
 },
 {
  "name": "buffs",
  "category": "observe",
  "status": "verified",
  "mechanism": "detalle del bloque del mundo: objetos de habilidad que empiezan por B",
  "latency": "Snapshot publicado",
  "signature": "buffs(u) -> 'list'",
  "doc": "Códigos de buff que tiene la unidad (p. ej. 'BHds' Escudo divino, 'Bslo' Ralentizar). Qué efecto corresponde a cada código: ver data/game/buffs.json."
 },
 {
  "name": "cooldown",
  "category": "observe",
  "status": "verified",
  "mechanism": "detalle del bloque del mundo: enfriamiento restante de la habilidad (temporizador de la habilidad)",
  "latency": "Snapshot publicado",
  "signature": "cooldown(u, ability: 'str') -> 'float | None'",
  "doc": "Segundos de enfriamiento que le quedan a esta habilidad (segundos de juego); 0 = se puede lanzar; si la unidad no tiene esa habilidad (o no tiene detalle) devuelve None."
 },
 {
  "name": "inventory",
  "category": "observe",
  "status": "verified",
  "mechanism": "detalle del bloque del mundo: inventario de 6 casillas",
  "latency": "Snapshot publicado",
  "signature": "inventory(hero) -> 'list | None'",
  "doc": "Códigos de cuatro caracteres de los objetos de las 6 casillas (casilla vacía = None); sin inventario devuelve None."
 },
 {
  "name": "current_order",
  "category": "observe",
  "status": "verified",
  "mechanism": "unidad del bloque del mundo: order / objetivo de la orden / punto objetivo de la orden",
  "latency": "Snapshot publicado",
  "signature": "current_order(u) -> 'dict | None'",
  "doc": "{'order','target','x','y'}: la orden que la unidad está ejecutando (order es 0x000D00xx o el código de cuatro caracteres de un edificio; 0 = ociosa).\ntarget es un par de handles; conviértelo en unidad con g.unit(target)."
 },
 {
  "name": "current_target",
  "category": "observe",
  "status": "verified",
  "mechanism": "unidad del bloque del mundo: objetivo de tarea",
  "latency": "Snapshot publicado",
  "signature": "current_target(u)",
  "doc": "La unidad a la que **realmente ataca / persigue** (si no hay, devuelve None).\n⚠ Tras una orden de ataque, el slot de orden se vacía enseguida y el ataque queda en la tarea: para saber \"a quién ataca\" usa esto, no current_order."
 },
 {
  "name": "clock",
  "category": "observe",
  "status": "verified",
  "mechanism": "cabecera del bloque del mundo clockMs (reloj de juego del motor)",
  "latency": "Snapshot publicado",
  "signature": "clock() -> 'float | None'",
  "doc": "Reloj de juego del motor (segundos de juego; 0 durante la carga). A velocidad aumentada avanza más rápido que el reloj real."
 },
 {
  "name": "production",
  "category": "observe",
  "status": "verified",
  "mechanism": "tabla de producción del bloque del mundo (objetos de habilidad Aque/ABnP/AUnP + tiempo transcurrido que sigue el runtime; error medido < 0.2 segundos de juego)",
  "latency": "Snapshot publicado",
  "signature": "production(building)",
  "doc": "Qué está haciendo este edificio: Production(kind, queue, duration, elapsed, blocked, progress, remaining…); si no hace nada, devuelve None.\n  kind 'queue' (entrenamiento / investigación / héroe; queue tiene como máximo 7 casillas, [0] es lo que se está haciendo) / 'construction' (en construcción) / 'upgrade' (mejora del edificio principal / de una torre);\n  blocked = hay cola pero no ha empezado (casi siempre falta comida: toca construir granjas); progress 0..1.\nTambién sirve para los edificios del rival (en modo justo, solo para los edificios visibles)."
 },
 {
  "name": "queue",
  "category": "observe",
  "status": "verified",
  "mechanism": "tabla de producción del bloque del mundo",
  "latency": "Snapshot publicado",
  "signature": "queue(building) -> 'list'",
  "doc": "Códigos de cuatro caracteres de la cola de entrenamiento / investigación ([0] es lo que se está haciendo); ocioso o no es un edificio de producción = []."
 },
 {
  "name": "all_production",
  "category": "observe",
  "status": "verified",
  "mechanism": "tabla de producción del bloque del mundo",
  "latency": "Snapshot publicado",
  "signature": "all_production(owner: 'str | int' = 'me') -> 'list'",
  "doc": "Toda la producción en curso [(edificio, Production)]. owner igual que en units(): 'me' / 'enemy' / número de jugador / 'all'.\nUso profesional: ver qué unidades entrena el rival, qué tecnologías investiga y cuándo sube de tier (cuando has explorado sus edificios)."
 },
 {
  "name": "path_distance",
  "category": "observe",
  "status": "verified",
  "mechanism": "bloque de mapa (IsTerrainPathable del motor) + bloque de árboles + huella de los edificios; A* en el lado del SDK (celdas de 128)",
  "latency": "Snapshot publicado + cálculo local",
  "signature": "path_distance(a, b) -> 'float | None'",
  "doc": "Distancia que recorre una unidad terrestre de a a b (a y b pueden ser unidades o (x,y)); si no se puede llegar, None. En mapas con islas, úsalo para saber \"si a este campamento de creeps / esta expansión se llega por tierra\";\nes más fiable que la distancia en línea recta (rodea bosques, acantilados y edificios). Precisión de una celda de 128; los huecos más estrechos que una celda se consideran cerrados."
 },
 {
  "name": "reachable",
  "category": "observe",
  "status": "verified",
  "mechanism": "igual que el anterior",
  "latency": "Snapshot publicado + cálculo local",
  "signature": "reachable(a, b) -> 'bool | None'",
  "doc": "Si se puede llegar por tierra (bloque de mapa aún sin calcular = None)."
 },
 {
  "name": "walk_path",
  "category": "observe",
  "status": "verified",
  "mechanism": "igual que el anterior",
  "latency": "Snapshot publicado + cálculo local",
  "signature": "walk_path(a, b) -> 'list | None'",
  "doc": "Puntos de giro del camino [(x,y)...] (el último es b); combínalo con path(units, lista_de_puntos) para que las tropas sigan ese camino (esquivar torres, ir por atajos)."
 },
 {
  "name": "upkeep",
  "category": "observe",
  "status": "inferred",
  "mechanism": "regla fija de la 1.27: 0~50 de comida sin mantenimiento, 51~80 ingresos ×0.7, 81~100 ×0.4",
  "latency": "Snapshot publicado",
  "signature": "upkeep(player: 'int | None' = None) -> 'dict | None'",
  "doc": "Nivel de mantenimiento: {'level': 'none'/'low'/'high', 'income': 1.0/0.7/0.4, 'next_at': comida del siguiente nivel (si no hay = None)}.\nSabiduría profesional: quédate en 50 de comida mientras subes a tier 3 / investigas mejoras de ataque y armadura, y sube a 80 solo antes del combate decisivo."
 },
 {
  "name": "xp_to_next",
  "category": "observe",
  "status": "verified",
  "mechanism": "bloque del mundo level/xp + fórmula NeedHeroXP de MiscGame",
  "latency": "Snapshot publicado",
  "signature": "xp_to_next(hero) -> 'int | None'",
  "doc": "Experiencia que le falta al héroe para el siguiente nivel (nivel 10 = 0)."
 },
 {
  "name": "creep_camps",
  "category": "observe",
  "status": "verified",
  "mechanism": "instantánea push (creeps a menos de 600 entre sí agrupados en un campamento) + nivel de units.json",
  "latency": "Snapshot publicado",
  "signature": "creep_camps(link: 'float' = 600.0) -> 'list'",
  "doc": "Agrupa en campamentos los creeps (visibles) del mapa: [{'x','y','units','level','hp','max_level'}], de más cerca a más lejos de nuestra base principal.\nlevel = nivel total del campamento (la medida habitual de la dificultad del creeping), hp = vida total. Combínalo con time_to_kill / path_distance para elegir campamento."
 },
 {
  "name": "buff_info",
  "category": "observe",
  "status": "verified",
  "mechanism": "data/game/buffs.json (BuffID de AbilityData.slk -> habilidad / efecto / duración)",
  "latency": "Datos locales",
  "signature": "buff_info(code: 'str') -> 'dict | None'",
  "doc": "Qué es un código de buff: {'ability','effect','dur','hero_dur','targets'} (p. ej. 'Bslo' -> Ralentizar). Si un código tiene varias filas, devuelve la primera."
 },
 {
  "name": "stats",
  "category": "observe",
  "status": "verified",
  "mechanism": "tablas de datos (UnitBalance/UnitWeapons/UpgradeData/MiscGame) + niveles de tecnología en tiempo real + nivel del héroe",
  "latency": "Snapshot publicado + carril rápido (un lote cada 5 s)",
  "signature": "stats(u, player: 'int | None' = None)",
  "doc": "Atributos de combate de la unidad, combat.UnitStats: vida / maná máximos, armadura (incluye mejoras de ataque y armadura y la agilidad del héroe), tipo de armadura, velocidad de movimiento, visión de día / de noche,\narmas (a qué puede atacar, alcance, intervalo de ataque, rango de daño, tipo de ataque, salpicadura). u puede ser una unidad (usa automáticamente las tecnologías de su dueño y el nivel del héroe) o un código de cuatro caracteres (player es nuestro jugador por defecto).\nCombínalo con .dps_vs(rival) / .hits_to_kill(rival) / combat.time_to_kill(grupo, rival). ⚠ No incluye objetos, auras ni buffs."
 },
 {
  "name": "time_to_kill",
  "category": "observe",
  "status": "verified",
  "mechanism": "stats() + vida en tiempo real",
  "latency": "Snapshot publicado",
  "signature": "time_to_kill(attackers, target) -> 'float | None'",
  "doc": "Cuántos segundos de juego tarda este grupo de unidades en matar a target atacando juntas (usa la vida actual de target; tiene en cuenta counters, armadura y mejoras de ataque y armadura; no tiene en cuenta el movimiento, la salpicadura ni la curación).\nUso profesional: al concentrar fuego, ataca primero al que \"muere más rápido\" (el menor time_to_kill), no al más cercano. Si no se le puede atacar = None."
 },
 {
  "name": "time_of_day",
  "category": "observe",
  "status": "verified",
  "mechanism": "zona de extensión del bloque del mundo: GetFloatGameState(GAME_STATE_TIME_OF_DAY)",
  "latency": "Snapshot publicado",
  "signature": "time_of_day() -> 'float | None'",
  "doc": "Hora del día dentro del juego (horas, 0~24). La partida empieza a las 8 de la mañana; un día completo = 480 segundos de juego (240 s de día y 240 s de noche, escalados por la velocidad del ciclo día/noche).\nSi no se puede leer (runtime antiguo / fuera de partida) devuelve None."
 },
 {
  "name": "is_night",
  "category": "observe",
  "status": "verified",
  "mechanism": "zona de extensión del bloque del mundo (de día de 6 a 18 h)",
  "latency": "Snapshot publicado",
  "signature": "is_night() -> 'bool | None'",
  "doc": "Si ahora es de noche (18:00~6:00). Jugada profesional: de noche los creeps duermen (atacas primero sin que te rodeen) y la visión de todas las unidades se reduce (buen momento para emboscadas);\nlos centinelas / unidades de los Elfos de la noche se vuelven invisibles de noche junto a los árboles. Si no se puede leer devuelve None."
 },
 {
  "name": "seconds_until",
  "category": "observe",
  "status": "verified",
  "mechanism": "zona de extensión del bloque del mundo + día de 480 s (medido: 20 segundos de juego por hora)",
  "latency": "Snapshot publicado",
  "signature": "seconds_until(hour: 'float') -> 'float | None'",
  "doc": "Segundos de juego que faltan para que el reloj del juego marque la hora hour (p. ej. seconds_until(18) = cuánto falta para el anochecer; útil para planificar el creeping nocturno)."
 },
 {
  "name": "items_on_ground",
  "category": "observe",
  "status": "verified",
  "mechanism": "bloque del mundo items[] (solo los del suelo: handle del portador todo FF)",
  "latency": "Snapshot publicado",
  "signature": "items_on_ground() -> 'list'",
  "doc": "Objetos en el suelo [Item(addr, handle_lo, handle_hi, type, x, y, life)]. Al recogerlos / usarlos se emite el evento item.removed."
 },
 {
  "name": "trees",
  "category": "observe",
  "status": "verified",
  "mechanism": "bloque de árboles Local\\War3Trees_<pid> (se refresca cada 2 segundos)",
  "latency": "Snapshot publicado",
  "signature": "trees(x: 'float | None' = None, y: 'float | None' = None, limit: 'int' = 60) -> 'list'",
  "doc": "Árboles vivos (los que tienen tree en targType de DestructableData); si das (x,y), se ordenan de más cerca a más lejos, como máximo limit.\nCada uno es un Tree(addr, handle_lo, handle_hi, type, x, y, life) y se puede pasar directamente a gather para talar."
 },
 {
  "name": "events",
  "category": "observe",
  "status": "verified",
  "mechanism": "anillo de eventos Local\\War3Events_<pid> (comparación de publicaciones + eventos de daño capturados por el runtime)",
  "latency": "Snapshot publicado",
  "signature": "events() -> 'list'",
  "doc": "Lo que ha pasado desde la última llamada: unit.appeared / unit.died / unit.removed / unit.damaged / order.changed /\nhero.levelup / owner.changed / item.appeared / item.removed / game.started (estos salen de comparar publicaciones; precisión = periodo de publicación, 50 ms),\nademás de damage / killed a nivel de motor (el runtime los registra en el hilo del juego en el momento en que ocurren, así que hay uno por **cada golpe**):\n    damage: handle = quien recibe el golpe, .source_addr = quien golpea (conviértelo en unidad con snapshot().unit_by_addr), .value = vida perdida real,\n            .raw_damage = daño antes de armadura, .attack_type (normal/pierce/siege/magic/chaos/hero/spell), .damage_type\n    killed: este golpe lo mató, .source_addr = el asesino\ny production.done, que el runtime obtiene siguiendo la tabla de producción (precisión = periodo de publicación): unidad = el edificio, .done_code = código de cuatro caracteres de lo terminado,\n    .done_kind = 'training' (unidad / héroe / resurrección) / 'research' / 'construction' (edificio terminado) / 'upgrade' (subida de tier / mejora de torre), .value = segundos de juego que tardó\nAñadidos el 09-25:\n    spell.cast: unidad = quien lanza, .spell código de cuatro caracteres de la habilidad, b nivel, value segundos de enfriamiento, x,y punto de lanzamiento (se detecta cuando la habilidad entra en enfriamiento; precisión = periodo de publicación)\n    player.left: .player número del jugador que se fue / fue eliminado por derrota; game.ended: se sale de la partida\n    selection.changed: cambió la selección del jugador local (obtén las unidades con g.selection())\n    message: una línea de un marco de mensajes de la pantalla (avisos del juego, chat, sistema): .text texto completo, .frame número del marco de mensajes,\n             .chat = {'channel', 'sender', 'text'} (si es chat; lo que el jugador escribe en el chat se lee de aquí)\n    ui.click / ui.hover / hotkey / mouse.world: interfaz y entrada (g.ui); .key es la key del lienzo / el atajo tal como se escribió\nCada uno es un Event(seq, kind, clock, addr, handle, type, owner, a, b, x, y, value, extra).\nEn modo justo (fair=True) solo llegan: eventos de tus propias unidades, eventos de unidades visibles en este momento (o que lo eran hace menos de 1 segundo), el daño que recibimos / que hacemos,\ny los eventos locales de interfaz / mensajes / partida."
 },
 {
  "name": "selection",
  "category": "observe",
  "status": "verified",
  "mechanism": "zona de extensión del bloque del mundo W3P, selAddrs (el runtime incluye la selección del jugador local en cada publicación)",
  "latency": "Snapshot publicado",
  "signature": "selection() -> 'list'",
  "doc": "Unidades que tiene seleccionadas ahora el jugador local (la unidad principal primero; como máximo 12). Cuando cambia la selección se emite el evento selection.changed."
 },
 {
  "name": "messages",
  "category": "observe",
  "status": "verified",
  "mechanism": "memoria compartida Local\\War3Msgs_<pid> (mensajes en pantalla capturados por el runtime)",
  "latency": "Snapshot publicado",
  "signature": "messages() -> 'list'",
  "doc": "Mensajes nuevos en los marcos de mensajes de la pantalla desde la última llamada: [{'text', 'frame', 'repeat', 'seq', 'game_ms'}].\nAquí están los avisos del juego («Necesitas más granjas», «No se puede construir ahí»), el chat y los mensajes del sistema; frame indica de qué marco de mensajes se trata.\nSon los mismos que los eventos message del flujo de eventos (cada uno con su propio cursor)."
 },
 {
  "name": "ui",
  "category": "control",
  "status": "verified",
  "mechanism": "W3P 74 input_enable + memoria compartida Local\\War3Input_<pid> (el runtime recibe la entrada de la ventana)",
  "latency": "Memoria compartida",
  "signature": "ui()",
  "doc": "Interfaz y entrada (openwar3.ui.UI): botones y tarjetas de elección clicables, atajos de teclado, clics en el suelo para elegir una posición, a qué apunta el ratón.\nEl juego no recibe el clic sobre un botón; es solo entrada local + dibujo local, así que también es seguro en multijugador."
 },
 {
  "name": "tech",
  "category": "observe",
  "status": "verified",
  "mechanism": "consulta W3P q_tech (recuento de tecnología del jugador en el motor)",
  "latency": "Carril rápido",
  "signature": "tech(code: 'str', player: 'int | None' = None) -> 'int | None'",
  "doc": "Nivel de investigación / número de edificios terminados (cuenta la cadena de mejoras: un Castillo también cuenta como htow). player es nuestro jugador por defecto; se puede consultar cualquier jugador."
 },
 {
  "name": "can_do",
  "category": "observe",
  "status": "verified",
  "mechanism": "consulta W3P q_feasible (comprobación de viabilidad del motor)",
  "latency": "Carril rápido",
  "signature": "can_do(u, code: 'str') -> 'int | None'",
  "doc": "Veredicto de viabilidad del motor: 0/220 se puede ordenar; 3 comida, 8 falta oro, 9 falta madera, 32 cola llena, 183 falta un requisito previo, 185 el altar está reviviendo, 221 no existe esa opción / en construcción.\n⚠ Para un trabajador que construye un edificio siempre da 221; no sirve para comprobar dónde colocarlo (usa build_near)."
 },
 {
  "name": "can_do_many",
  "category": "observe",
  "status": "verified",
  "mechanism": "consulta W3P q_feasible × N, enviadas en un lote",
  "latency": "Carril rápido × 1",
  "signature": "can_do_many(pairs) -> 'list'",
  "doc": "Muchos can_do de una vez: pairs = [(unidad, código de cuatro caracteres), ...]; devuelve la lista de códigos de veredicto en el mismo orden (los que no se pudieron consultar son None).\nAl planificar qué construir / entrenar en un tick, pregúntalo todo junto primero: es N veces más rápido que ir can_do por can_do (cerebro de referencia, 09-23: planificación de construcción de 76 -> 25 ms)."
 },
 {
  "name": "tech_many",
  "category": "observe",
  "status": "verified",
  "mechanism": "consulta W3P q_tech × N, enviadas en un lote",
  "latency": "Carril rápido × 1",
  "signature": "tech_many(codes, player: 'int | None' = None) -> 'dict'",
  "doc": "Consulta de una vez muchos recuentos de tecnología / edificios: {código de cuatro caracteres: cantidad o None}."
 },
 {
  "name": "visible",
  "category": "observe",
  "status": "verified",
  "mechanism": "consulta W3P q_visible (visible / niebla de guerra / zona negra)",
  "latency": "Carril rápido",
  "signature": "visible(x: 'float', y: 'float') -> 'bool | None'",
  "doc": "Si ahora vemos este punto (no está en la niebla de guerra / zona negra). Un bot en modo justo solo debería usar enemigos visibles."
 },
 {
  "name": "gold_left",
  "category": "observe",
  "status": "inferred",
  "mechanism": "consulta W3P q_mine_gold (oro restante en la mina según el motor)",
  "latency": "Carril rápido",
  "signature": "gold_left(mine) -> 'int | None'",
  "doc": "Cuánto oro le queda a la mina."
 },
 {
  "name": "enemy_ai_plan",
  "category": "observe",
  "status": "verified",
  "mechanism": "consulta W3P q_captain (el capitán del ordenador al que sigue la unidad enemiga)",
  "latency": "Carril rápido",
  "signature": "enemy_ai_plan(enemy_unit) -> 'dict | None'",
  "doc": "El capitán de la IA del ordenador: adónde lleva a sus tropas (antes de salir ya sabe qué parte de tu base va a atacar). Solo funciona contra rivales del ordenador; si la unidad no sigue a un capitán, devuelve None."
 },
 {
  "name": "batch",
  "category": "command",
  "status": "verified",
  "mechanism": "los comandos del bloque se acumulan en un lote que se envía de una vez al cerrar el bloque (se ejecutan en el mismo frame; una sola espera al hilo del juego)",
  "latency": "Carril rápido × 1",
  "signature": "batch() -> 'Batch'",
  "doc": "Agrupa en un lote los comandos de un tick:\n\n    with g.batch() as b:\n        g.attack(archers, target)          # devuelve Pending; se convierte en recibo al cerrar el bloque\n        g.move(wounded, *home)\n        g.cast(hero, \"thunderclap\")\n    print(b.sent, b.wait_ms, [r.reason for r in b.receipts])\n\nCada comando enviado por separado espera una vez a que lo procese el hilo del juego (unos 10 ms); un lote espera una sola vez: así el cerebro de referencia (09-23) bajó una ronda de 48 -> 26 ms.\n* El arbitraje sigue pasando comando a comando (una unidad reservada recibe en el acto un recibo held y no entra en el lote);\n* Dentro del bloque los comandos devuelven Pending: leer su .ok antes de cerrar el bloque lanza un error (el recibo aún no existe); después se usa igual que un Receipt;\n* Una excepción dentro del bloque = se descarta el lote entero (status 97 cancelled) y se liberan las unidades reservadas;\n* Las consultas (can_do / tech / visible …), build_near y buy no entran en el lote y se preguntan en el acto, como siempre: su resultado se necesita al momento;\n  para preguntar muchas cosas de una vez usa can_do_many / tech_many;\n* Un with g.batch() anidado se une al lote más externo; con más de 16 comandos, el runtime lo divide automáticamente en tramos (una espera por tramo)."
 },
 {
  "name": "order_of",
  "category": "observe",
  "status": "verified",
  "mechanism": "orden de la instantánea + comandos de este proceso recién aceptados (recibos)",
  "latency": "Snapshot publicado",
  "signature": "order_of(u) -> 'int | None'",
  "doc": "La orden actual de la unidad, **incluida la que acabas de dar en este tick** (mientras la instantánea no se pone al día, usa la orden nueva del recibo).\n⚠ Partida real del 09-23: hello_bot acababa de mandar a un campesino a construir una granja y, en el mismo tick, rush_bot lo vio \"ocioso\" en la instantánea y lo mandó a construir un cuartel; la granja se quedaba a medias una y otra vez.\n  Para elegir unidades \"ociosas / que no están construyendo\", usa esto en lugar de u.order."
 },
 {
  "name": "move",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P point: move (bits de extra = modo de cola)",
  "latency": "Carril rápido",
  "signature": "move(units, x: 'float', y: 'float', queue: 'str | None' = None)",
  "doc": "Ir a (x,y) sin atacar por el camino (úsalo para retirarte). Puedes pasar una unidad o una lista (reciben la orden juntas en el mismo frame).\nqueue='after': ir después de terminar lo que está haciendo (se inserta tras la orden actual). values[0] del recibo = cuántas órdenes tiene en cola la unidad tras recibir esta (incluida la que está ejecutando)."
 },
 {
  "name": "attack_move",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P point: attack a un punto",
  "latency": "Carril rápido",
  "signature": "attack_move(units, x: 'float', y: 'float', queue: 'str | None' = None)",
  "doc": "Atacar-mover (A sobre el suelo): ataca a los enemigos que encuentre por el camino. queue igual que en move."
 },
 {
  "name": "attack",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target: comando a un objetivo (clic derecho, smart)",
  "latency": "Carril rápido",
  "signature": "attack(units, target, force: 'bool' = False, queue: 'str | None' = None)",
  "doc": "Atacar a target. Por defecto usa el clic derecho (sobre un enemigo = atacar a ese en concreto; medido el 09-23: el objetivo de la orden y el objetivo de tarea son esa unidad).\n⚠ El objetivo tiene que estar en tu campo de visión; si no se ve, se rechaza (código de motivo 1001).\nforce=True usa la orden de ataque 0x0F (necesaria para atacar a unidades propias / animalillos neutrales). Medido: solo cambia a la orden de ataque sin recordar el objetivo,\ny la unidad se va a atacar a otro enemigo cercano; no la uses para atacar a un objetivo concreto."
 },
 {
  "name": "stop",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P immediate: stop",
  "latency": "Carril rápido",
  "signature": "stop(units)",
  "doc": "Detiene todo lo que esté haciendo (ID de orden 0x000D0004) y también vacía las órdenes en cola."
 },
 {
  "name": "hold",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P immediate: holdposition",
  "latency": "Carril rápido",
  "signature": "hold(units, queue: 'str | None' = None)",
  "doc": "Mantener posición (no persigue; solo ataca lo que está a su alcance)."
 },
 {
  "name": "patrol",
  "category": "command",
  "status": "inferred",
  "mechanism": "W3P point: patrol",
  "latency": "Carril rápido",
  "signature": "patrol(units, x: 'float', y: 'float', queue: 'str | None' = None)",
  "doc": "Patrulla entre la posición actual y (x,y)."
 },
 {
  "name": "attack_ground",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P point: attackground (unidades de asedio / morteros / catapultas)",
  "latency": "Carril rápido",
  "signature": "attack_ground(units, x: 'float', y: 'float', queue: 'str | None' = None)",
  "doc": "Atacar el suelo: la artillería dispara a una zona (contra unidades invisibles, contra lo que hay detrás de un bosque, para cerrar un paso). Solo lo aceptan las unidades que pueden atacar el suelo."
 },
 {
  "name": "cancel",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P immediate: cancel",
  "latency": "Carril rápido",
  "signature": "cancel(building)",
  "doc": "Cancela: la última casilla de la cola de entrenamiento / investigación (devuelve el dinero), un edificio en construcción (devuelve el 75 %) o un edificio principal que se está mejorando."
 },
 {
  "name": "path",
  "category": "command",
  "status": "verified",
  "mechanism": "un lote: el primer tramo se ejecuta al momento y el resto se inserta en orden inverso con queue='after' (el motor solo permite insertar tras la orden actual)",
  "latency": "Carril rápido × 1",
  "signature": "path(units, points, attack: 'bool' = False)",
  "doc": "Recorre una serie de puntos en orden (puntos encadenados con Shift: waypoints, esquivar torres, rutas de exploración). Con attack=True, cada tramo es un atacar-mover.\nSe envía de una vez; hay un recibo por punto (en el orden de points)."
 },
 {
  "name": "gather",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target: harvest (mina de oro o árbol)",
  "latency": "Carril rápido",
  "signature": "gather(workers, target, queue: 'str | None' = None)",
  "doc": "Recolectar oro / talar (target es una mina de oro o un árbol de trees()). ⚠ Asígnalo solo a trabajadores ociosos (idle_workers): repetir la orden a uno con tarea interrumpe el ciclo de recolección.\nUso profesional: volver a minar al terminar de construir = gather(worker, mine, queue='after') después de build(...)."
 },
 {
  "name": "repair",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target: repair",
  "latency": "Carril rápido",
  "signature": "repair(workers, building, queue: 'str | None' = None)",
  "doc": "Reparar / ayudar a construir (las obras de Humanos y Orcos se detienen si nadie construye)."
 },
 {
  "name": "build",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P build: orden de construcción; se confirma leyendo la orden del trabajador en el mismo frame",
  "latency": "Carril rápido",
  "signature": "build(worker, code: 'str', x: 'float', y: 'float', queue: 'str | None' = None)",
  "doc": "Hace que el trabajador construya code en (x,y) (coordenadas alineadas a 32). Recibo aceptado = la orden del trabajador ya es ese edificio (o la orden de empezar la obra);\ncon queue='after' = entró en la cola de órdenes del trabajador (values[0] del recibo = número en cola).\n⚠ Aceptado ≠ construido: el motor también acepta en el acto un punto dentro de un bosque, y el trabajador solo falla al llegar (medido el 09-23); si el dinero se gasta en otra cosa, los cimientos tampoco aparecen.\nSi no sabes dónde cabe, usa build_near (sigue el resultado y pone en la lista negra los puntos que fallan). Para construir varios seguidos, usa build_queue."
 },
 {
  "name": "build_queue",
  "category": "command",
  "status": "verified",
  "mechanism": "un lote: el primero al momento y el resto en orden inverso con queue='after'",
  "latency": "Carril rápido × 1",
  "signature": "build_queue(worker, plan)",
  "doc": "Un trabajador construye varios edificios seguidos en orden (encadenados con Shift): plan = [(código de cuatro caracteres, x, y), ...]. Se envía de una vez; los recibos siguen el orden de plan.\n⚠ El dinero se descuenta al empezar cada obra (no al encolar): si encolas 3 y solo tienes dinero para 1, los otros dos fallarán cuando el trabajador llegue."
 },
 {
  "name": "build_near",
  "category": "command",
  "status": "verified",
  "mechanism": "build punto a punto + seguimiento (aparecen los cimientos = éxito; el trabajador abandona la orden sin cimientos = ese punto a la lista negra)",
  "latency": "Carril rápido × puntos probados",
  "signature": "build_near(worker, code: 'str', x: 'float', y: 'float', min_r: 'float' = 450, max_r: 'float' = 1500, max_tries: 'int' = 24)",
  "doc": "Busca alrededor de (x,y), de cerca a lejos, un punto donde quepa code y lo construye. **No bloquea**; puedes llamarlo en cada tick:\n  * hay un intento de este edificio todavía en curso (el trabajador va de camino) -> devuelve ese punto y no repite la orden;\n  * el intento anterior tuvo éxito (aparecieron los cimientos) -> esta vez busca un punto nuevo si hace falta;\n  * el intento anterior falló (el trabajador llegó y vio que no cabía; el motor retiró la orden y no hay cimientos) -> ese punto va a la lista negra 45 segundos y se prueba el siguiente;\n  * falta dinero -> devuelve None directamente (ni prueba ni pone en la lista negra); si se agotan los puntos, devuelve None.\n⚠ Por qué hace falta el seguimiento: en partidas reales del 09-23, el motor **aceptaba en el acto** un punto dentro de un bosque y el trabajador solo fallaba al llegar (el recibo del mismo frame no puede detectarlo);\n  y la comprobación de ubicación del motor siempre devuelve 221 para un trabajador que construye, así que tampoco se puede \"consultar\" antes de construir. Solo los puntos claramente ocupados (el centro del ayuntamiento) se rechazan en el acto."
 },
 {
  "name": "can_afford",
  "category": "observe",
  "status": "verified",
  "mechanism": "nuestros recursos de la instantánea push + precios de units.json",
  "latency": "Snapshot publicado",
  "signature": "can_afford(code: 'str') -> 'bool'",
  "doc": "Si el oro / la madera actuales alcanzan para comprar code (unidades, edificios; según los precios de units.json). Lo que no está en la tabla de precios se considera siempre asequible.\n⚠ Los códigos de subida de tier tienen precio acumulado en la tabla, así que aquí el resultado es conservador; lo que cuenta al final es el recibo del motor."
 },
 {
  "name": "train",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P immediate: código de cuatro caracteres; si se rechaza, incluye el código de motivo de viabilidad",
  "latency": "Carril rápido",
  "signature": "train(building, code: 'str')",
  "doc": "Entrena unidades / investiga tecnologías / mejora el edificio principal (subir de tier = dar al propio edificio principal el código de cuatro caracteres del edificio de destino, p. ej. 'hkee').\nSi se rechaza, el reason del recibo dice por qué (falta comida, falta oro, falta madera, cola llena, falta un requisito previo…)."
 },
 {
  "name": "learn",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P learn: solo cuenta como aprendida si bajan los puntos de habilidad",
  "latency": "Carril rápido",
  "signature": "learn(hero, ability: 'str')",
  "doc": "El héroe aprende una habilidad (código de cuatro caracteres, p. ej. 'AHbz' Ventisca)."
 },
 {
  "name": "cast",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target / point / immediate (según los parámetros)",
  "latency": "Carril rápido",
  "signature": "cast(u, spell, target=None, x: 'float | None' = None, y: 'float | None' = None)",
  "doc": "Lanza un hechizo. spell es un nombre de orden ('thunderbolt' Martillo de tormenta, 'blizzard', 'holybolt' Luz sagrada…; ver data/order-ids.txt) o un ID de orden.\nCon target = sobre una unidad; con x,y = sobre el suelo; sin ninguno = sin objetivo (Atronar, Escudo divino, Invocar elemental de agua).\nQue el recibo diga aceptado solo significa que el motor lo aceptó; para saber si se lanzó, mira si cooldown() entró en enfriamiento o si apareció algo en buffs()."
 },
 {
  "name": "rally",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P rally",
  "latency": "Carril rápido",
  "signature": "rally(building, x: 'float | None' = None, y: 'float | None' = None, target=None)",
  "doc": "Fija el punto de reunión (en un punto, o sobre una unidad / mina de oro)."
 },
 {
  "name": "revive",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P revive: lista de héroes muertos -> el altar lanza la resurrección sobre el héroe muerto",
  "latency": "Carril rápido",
  "signature": "revive(altar, hero=None)",
  "doc": "Revive en el altar a un héroe muerto (si no das hero, revive al primero de la lista).\nMotivos de rechazo frecuentes (aparecen en el reason del recibo): falta comida (los héroes también ocupan comida), falta dinero, murió hace muy poco (solo puede revivir unos 3 segundos de juego después de morir),\nya hay una resurrección en curso (al aceptarla, el motor vacía esa casilla en el acto)."
 },
 {
  "name": "pick_up",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target: clic derecho sobre el objeto",
  "latency": "Carril rápido",
  "signature": "pick_up(hero, item)",
  "doc": "El héroe va a recoger un objeto del suelo (item sale de items_on_ground). Al recogerlo aparece en el inventario y se emite el evento item.removed para el suelo."
 },
 {
  "name": "use_item",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P use_item (por número de casilla)",
  "latency": "Carril rápido",
  "signature": "use_item(hero, slot: 'int', target=None, x: 'float | None' = None, y: 'float | None' = None)",
  "doc": "Usa el objeto de la casilla slot (0~5) del inventario; admite una unidad objetivo o un punto objetivo.\n⚠ Al usar un objeto sobre un punto (p. ej. Ivory Tower), el motor devuelve 0 incluso si tiene éxito, así que el recibo siempre cuenta como aceptado: mira si esa casilla del inventario se vació."
 },
 {
  "name": "drop_item",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P item_drop (copia de JASS UnitDropItemPoint: dropitem 0xD0021 a un punto + el objeto como objetivo inmediato)",
  "latency": "Carril rápido",
  "signature": "drop_item(hero, slot: 'int', x: 'float', y: 'float')",
  "doc": "Suelta en (x,y) lo que haya en la casilla slot del inventario (el héroe camina hasta allí y lo deja)."
 },
 {
  "name": "give_item",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P item_drop (copia de JASS UnitDropItemTarget: dropitem sobre una unidad)",
  "latency": "Carril rápido",
  "signature": "give_item(hero, slot: 'int', to)",
  "doc": "Da lo que haya en la casilla slot del inventario a to (otro héroe / unidad; camina hasta él y se lo entrega). Dárselo a una tienda = venderlo (ver sell_item)."
 },
 {
  "name": "sell_item",
  "category": "command",
  "status": "verified",
  "mechanism": "igual que give_item, con una tienda como objetivo (medido: el Staff of Sanctuary se vende por 125 de oro)",
  "latency": "Carril rápido",
  "signature": "sell_item(hero, slot: 'int', shop)",
  "doc": "Vende a una tienda lo que haya en la casilla slot del inventario (el héroe tiene que ir junto a la tienda; solo acepta objetos vendibles y devuelve la mitad del precio)."
 },
 {
  "name": "move_item",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target: orden 0xD0022 + número de casilla, objetivo = el objeto (copia de JASS UnitDropItemSlot)",
  "latency": "Carril rápido",
  "signature": "move_item(hero, slot: 'int', to_slot: 'int')",
  "doc": "Cambia de casilla dentro del inventario (de la casilla slot a la casilla to_slot; si ambas tienen algo, se intercambian). Útil para ordenar las teclas rápidas."
 },
 {
  "name": "buy",
  "category": "command",
  "status": "inferred",
  "mechanism": "W3P buy: la tienda vende al héroe que está al lado",
  "latency": "Carril rápido",
  "signature": "buy(shop, item_code: 'str')",
  "doc": "Compra un objeto en una tienda (para el héroe que está junto a la tienda). Si falta un requisito tecnológico, el motor devuelve 0 y no cobra."
 },
 {
  "name": "call_to_arms",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P immediate: townbellon/off",
  "latency": "Carril rápido",
  "signature": "call_to_arms(hall, on: 'bool' = True)",
  "doc": "Llamada a las armas de los Humanos: los campesinos se convierten en milicia (el ayuntamiento de tier 1 no tiene esta habilidad; solo funciona en el Torreón / Castillo)."
 },
 {
  "name": "set_speed",
  "category": "control",
  "status": "verified",
  "mechanism": "acción 47 (25~800%)",
  "latency": "Canal de control",
  "signature": "set_speed(percent: 'int') -> 'bool'",
  "doc": "Velocidad de juego (100 = velocidad normal)."
 },
 {
  "name": "pause",
  "category": "control",
  "status": "verified",
  "mechanism": "W3P pause",
  "latency": "Carril rápido",
  "signature": "pause(on: 'bool' = True)",
  "doc": "Pausa / reanuda el juego. Durante la pausa el reloj del motor se detiene, pero por el carril rápido se pueden seguir dando órdenes (el despacho de eventos sigue funcionando)."
 },
 {
  "name": "set_publish_period",
  "category": "control",
  "status": "verified",
  "mechanism": "bloque del mundo requestedPeriodMs",
  "latency": "Snapshot publicado",
  "signature": "set_publish_period(ms: 'int') -> 'None'",
  "doc": "Periodo de publicación del estado del mundo (16~1000 milisegundos; por defecto 50). Cada recopilación cuesta unos 0.5 ms, así que 33 ms no es problema; el valor es único para toda la máquina y gana el último que se escribe."
 },
 {
  "name": "say",
  "category": "control",
  "status": "verified",
  "mechanism": "acción 56",
  "latency": "Canal de control",
  "signature": "say(u, text: 'str', seconds: 'float' = 4.0) -> 'bool'",
  "doc": "Muestra un bocadillo de chat sobre la unidad (para retransmisiones / depuración; no afecta al juego). Devuelve False si el bocadillo no apareció; el motivo queda en g.last_say_error."
 },
 {
  "name": "message",
  "category": "control",
  "status": "inferred",
  "mechanism": "acción 45",
  "latency": "Canal de control",
  "signature": "message(text: 'str') -> 'bool'",
  "doc": "Escribe una línea en la zona de mensajes de la esquina inferior izquierda del juego (solo se ve en esta máquina). Hay que esperar a que el juego muestre antes un aviso propio (la DLL captura el cuadro de mensajes a partir de ese aviso)."
 },
 {
  "name": "end_game",
  "category": "control",
  "status": "verified",
  "mechanism": "acción 22",
  "latency": "Canal de control",
  "signature": "end_game() -> 'bool'",
  "doc": "Termina este proceso del juego (farm.py --keep abre automáticamente la siguiente partida según next_game.json)."
 },
 {
  "name": "canvas",
  "category": "control",
  "status": "verified",
  "mechanism": "W3P 73 canvas_enable + memoria compartida Local\\War3Canvas_<pid> (el runtime lo dibuja en cada fotograma justo antes de que el juego dibuje el puntero del ratón; el puntero queda por encima)",
  "latency": "Memoria compartida",
  "signature": "canvas()",
  "doc": "Lienzo: dibuja sobre la pantalla del juego cuadros de texto, paneles, barras de progreso, imágenes, círculos en el suelo y rutas (openwar3.canvas.Canvas).\nLo dibuja el propio runtime, sin crear handles del juego ni cambiar su estado: es seguro también en multijugador; estilo libre (chino, esquinas redondeadas, transparencia)."
 },
 {
  "name": "press_to_continue",
  "category": "control",
  "status": "verified",
  "mechanism": "PostMessage WM_KEYDOWN/UP de la barra espaciadora a la ventana del juego (sin robar el foco)",
  "latency": "Mensaje de ventana",
  "signature": "press_to_continue() -> 'bool'",
  "doc": "Pulsa una vez la barra espaciadora en la pantalla de carga de \"Pulsa cualquier tecla para continuar\". Muchos mapas RPG / de campaña necesitan una tecla al terminar de cargar para empezar (medido el 09-24 con WarChasers:\nsin pulsarla se queda en la pantalla de carga, con el reloj de juego a 0 y el carril rápido sin vaciarse). openwar3.run la pulsa solo mientras espera a entrar en la partida; normalmente no hace falta llamarla a mano."
 },
 {
  "name": "map_data",
  "category": "observe",
  "status": "verified",
  "mechanism": "archivo del mapa (la ruta --map del lanzador): w3u/w3t/w3a + wts; en mapas protegidos lee los TXT del propio mapa",
  "latency": "Lee archivo (la 1.ª vez ~0.1 s)",
  "signature": "map_data()",
  "doc": "Datos del mapa que se está jugando (openwar3.mapdata.MapData): name_of('HC07') da el nombre de unidades / objetos / habilidades personalizados; también hero_names y tooltip.\nEn los mapas RPG la mayoría de las unidades las crea el propio mapa y no están en la tabla de nombres integrada; si la partida no la arrancó el lanzador (no se encuentra el archivo del mapa), devuelve None."
 },
 {
  "name": "jass",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "W3P 70 jass (el runtime busca la native por nombre en su tabla de 1291)",
  "latency": "Carril rápido",
  "signature": "jass()",
  "doc": "Llama a cualquier native de JASS por su nombre: g.jass.CreateUnit(g.jass.Player(1), \"Hpal\", x, y, 270.0).\nLos parámetros I/R/B/S/H se convierten solos (las unidades y objetos se pasan tal cual); en multijugador solo se pueden llamar las de solo lectura. Detalles en openwar3/jass.py y docs/COMPANION_ZH.md."
 },
 {
  "name": "player_slots",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "JASS GetPlayerController / GetPlayerSlotState / IsPlayerAlly",
  "latency": "Carril rápido",
  "signature": "player_slots() -> 'list[dict]'",
  "doc": "Los 16 slots de jugador: controller (user = persona real / computer / neutral…), state (empty / playing / left), human, me, ally (si es aliado mío).\nEn mapas RPG sirve para buscar un slot libre donde poner al compañero y para saber si la partida es de un jugador."
 },
 {
  "name": "spawn",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "JASS CreateUnit + W3P 72 handle -> unidad",
  "latency": "Carril rápido",
  "signature": "spawn(code: 'str', x: 'float', y: 'float', player: 'int | None' = None, facing: 'float' = 270.0)",
  "doc": "Crea una unidad en (x,y) (player es por defecto el jugador local) y devuelve la unidad de la instantánea (espera a la siguiente publicación del mundo, ~50 ms); si no se puede crear, devuelve None.\nLa unidad devuelta tiene un atributo más, jass_handle. ⚠ Solo funciona en partidas de un jugador (en multijugador se desincroniza)."
 },
 {
  "name": "set_alliance",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "JASS SetPlayerAlliance",
  "latency": "Carril rápido",
  "signature": "set_alliance(a: 'int', b: 'int', allied: 'bool' = True, vision: 'bool' = True, control: 'bool' = False, xp: 'bool' = False, both: 'bool' = True) -> 'None'",
  "doc": "Fija la relación de alianza del jugador a hacia b: allied = no atacarse + pedirse ayuda; vision = visión compartida; control = control compartido de unidades (b puede mandar las unidades de a);\nxp = experiencia compartida. both=True fija los dos sentidos a la vez (control solo se fija de a -> b)."
 },
 {
  "name": "set_player_name",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "JASS SetPlayerName",
  "latency": "Carril rápido",
  "signature": "set_player_name(player: 'int', name: 'str') -> 'None'",
  "doc": "Cambia el nombre del jugador (el que aparece en el marcador, el chat y el panel de aliados). Sirve para ponerle nombre al compañero."
 },
 {
  "name": "show_text",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "JASS DisplayTimedTextToPlayer",
  "latency": "Carril rápido",
  "signature": "show_text(text: 'str', seconds: 'float' = 6.0, player: 'int | None' = None) -> 'None'",
  "doc": "Muestra una línea de texto en la esquina inferior izquierda de la pantalla (el tipo de texto que usan los disparadores del mapa); por defecto, para el jugador local. Admite códigos de color |cffRRGGBB."
 }
]