[
 {
  "name": "status",
  "category": "meta",
  "status": "verified",
  "mechanism": "Bloc du monde + voie rapide + table des réservations",
  "latency": "Calcul local",
  "signature": "status() -> 'dict'",
  "doc": "État de la connexion : pid, publication du monde (période, durée de collecte), compteurs de la voie rapide."
 },
 {
  "name": "snapshot",
  "category": "observe",
  "status": "verified",
  "mechanism": "Bloc du monde W3P Local\\War3World_<pid> (poussé par le runtime toutes les 50 ms, seqlock)",
  "latency": "Instantané poussé",
  "signature": "snapshot(max_age: 'float' = 0.05)",
  "doc": "État complet de toute la carte (WorldState) : .units .players .items .clock .me ; des appels répétés dans un délai de max_age secondes renvoient la même copie.\n⚠ Les ouvriers entrés dans une mine d'or ne figurent pas dans la liste ; par défaut toute la carte est visible (en modèle lockstep, tout est présent en local), seul Game(fair=True) filtre selon le champ de vision."
 },
 {
  "name": "last_seen",
  "category": "observe",
  "status": "verified",
  "mechanism": "visibleTo de l'instantané poussé (à chaque rafraîchissement de l'instantané, les unités ennemies/creeps visibles sont enregistrées)",
  "latency": "Instantané poussé",
  "signature": "last_seen(owner: 'str | int' = 'enemy', max_age: 'float | None' = None) -> 'list'",
  "doc": "Unités ennemies (ou creeps avec 'creep', ou unités d'un numéro de joueur) vues pour la dernière fois : [(état de l'unité à ce moment-là, horloge de jeu à ce moment-là, secondes écoulées depuis)], les plus récentes en premier.\nUne unité vue en train de mourir est retirée de la liste. En mode équitable comme en mode normal, l'enregistrement se fait selon « ce que votre camp voit en ce moment » — c'est la carte que le joueur a en tête :\nles forces repérées, la dernière position connue du héros adverse, le moment où l'adversaire a pris son expansion. max_age ne garde que les unités vues dans ce nombre de secondes de jeu."
 },
 {
  "name": "map",
  "category": "observe",
  "status": "verified",
  "mechanism": "Bloc de carte W3P Local\\War3Map_<pid> (calculé par lots par le runtime après le début de la partie, IsTerrainPathable déplacement/construction)",
  "latency": "Instantané poussé",
  "signature": "map()",
  "doc": "Table du terrain de la partie, MapInfo : .walkable(x,y) .buildable(x,y) .at(x,y) .bounds (zone jouable) .starts (points de départ) .cells (bit0 non praticable, bit1 non constructible).\nLe calcul prend quelques secondes après le début de la partie ; tant qu'il n'est pas terminé, renvoie None. Les arbres n'y figurent pas (utilisez trees())."
 },
 {
  "name": "me",
  "category": "observe",
  "status": "verified",
  "mechanism": "En-tête du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "me() -> 'int | None'",
  "doc": "Votre numéro de joueur (0~11)."
 },
 {
  "name": "resources",
  "category": "observe",
  "status": "verified",
  "mechanism": "players[16] du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "resources(player: 'int | None' = None) -> 'dict | None'",
  "doc": "{'gold','lumber','food_used','food_cap','gold_gathered','lumber_gathered'} ; player vaut par défaut votre joueur, et n'importe quel joueur peut être lu.\nRenvoie None si la lecture échoue : ne le confondez pas avec 0."
 },
 {
  "name": "players",
  "category": "observe",
  "status": "verified",
  "mechanism": "players[16] du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "players() -> 'list'",
  "doc": "Les 16 emplacements de joueur : Player(id, gold, lumber, food_used, food_cap, gold_gathered, lumber_gathered, race, known)."
 },
 {
  "name": "units",
  "category": "observe",
  "status": "verified",
  "mechanism": "units[] du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "units(owner: 'str | int' = 'all', types=None, alive: 'bool' = True) -> 'list'",
  "doc": "Filtre les unités par propriétaire / type. owner : 'me' / 'enemy' / 'creep' / 'all' / numéro de joueur. types : ensemble de codes à quatre caractères."
 },
 {
  "name": "unit",
  "category": "observe",
  "status": "verified",
  "mechanism": "by_handle du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "unit(handle) -> 'object | None'",
  "doc": "Trouve une unité par sa paire de handles (lo, hi) (la cible d'ordre, la cible de tâche et les événements fournissent tous des paires de handles)."
 },
 {
  "name": "is_building",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané + units.json (spd==0 = bâtiment)",
  "latency": "Instantané poussé",
  "signature": "is_building(u) -> 'bool'",
  "doc": "Indique s'il s'agit d'un bâtiment (tours comprises). Déterminé par une vitesse de déplacement nulle dans la table des unités ; l'emprise au sol du hall mort-vivant vaut 0, ne vous fiez donc pas à l'emprise."
 },
 {
  "name": "my_workers",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané poussé",
  "latency": "Instantané poussé",
  "signature": "my_workers() -> 'list'",
  "doc": "Vos ouvriers (paysans / péons / acolytes / feux follets)."
 },
 {
  "name": "idle_workers",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané poussé (emplacement d'ordre + emplacement de tâche)",
  "latency": "Instantané poussé",
  "signature": "idle_workers() -> 'list'",
  "doc": "Ouvriers sans travail : ni ordre ni tâche (ceux à qui vous venez de donner du travail pendant ce tick ne comptent pas).\n⚠ Redonner un ordre de récolte à un ouvrier qui a une tâche interrompt son cycle de récolte (revenu nul)."
 },
 {
  "name": "my_heroes",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané poussé",
  "latency": "Instantané poussé",
  "signature": "my_heroes() -> 'list'",
  "doc": "Vos héros vivants (les héros morts sont dans la liste de résurrection de l'autel, voir revive)."
 },
 {
  "name": "my_army",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané poussé + units.json",
  "latency": "Instantané poussé",
  "signature": "my_army() -> 'list'",
  "doc": "Vos unités de combat : ni ouvriers ni bâtiments."
 },
 {
  "name": "my_buildings",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané poussé",
  "latency": "Instantané poussé",
  "signature": "my_buildings(types=None) -> 'list'",
  "doc": "Vos bâtiments (tours et fondations en construction comprises) ; types permet de n'en garder que certains, par exemple {'hbar'}."
 },
 {
  "name": "is_constructing",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané poussé (ordre = code à quatre caractères d'un bâtiment, ou ordre de construction / de réparation)",
  "latency": "Instantané poussé",
  "signature": "is_constructing(worker) -> 'bool'",
  "doc": "Indique si cet ouvrier est en train de construire (ou se rend sur un chantier / aide à réparer ; y compris s'il vient d'y être envoyé pendant ce tick). Ignorez-le quand vous choisissez un constructeur, sinon le chantier précédent s'arrête."
 },
 {
  "name": "under_construction",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané poussé (les PV des fondations montent d'une valeur très basse jusqu'au maximum)",
  "latency": "Instantané poussé",
  "signature": "under_construction(building) -> 'bool'",
  "doc": "Ce bâtiment n'est pas terminé (PV incomplets). ⚠ Un bâtiment endommagé n'a pas non plus tous ses PV — suffisant en début de partie, mais une fois les combats engagés, tenez aussi compte du temps."
 },
 {
  "name": "gold_mines",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané poussé (ngol/egol/ugol)",
  "latency": "Instantané poussé",
  "signature": "gold_mines() -> 'list'",
  "doc": "Les mines d'or de la carte. ⚠ La mine d'or enchevêtrée des Elfes de la nuit et la mine neutre ont chacune une unité aux mêmes coordonnées ; envoyez la récolte vers la vôtre."
 },
 {
  "name": "enemies",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané poussé",
  "latency": "Instantané poussé",
  "signature": "enemies(fighters_only: 'bool' = False) -> 'list'",
  "doc": "Unités des joueurs ennemis (creeps exclus). fighters_only : exclut les ouvriers et les bâtiments."
 },
 {
  "name": "creeps",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané poussé (owner 12 = neutre hostile)",
  "latency": "Instantané poussé",
  "signature": "creeps() -> 'list'",
  "doc": "Creeps (neutres hostiles). ⚠ La nuit, la vision diminue ; une fois les camps éloignés passés dans le brouillard, les commandes sur cible qui les visent sont rejetées (code de motif 1001)."
 },
 {
  "name": "nearest",
  "category": "meta",
  "status": "verified",
  "mechanism": "Calcul pur",
  "latency": "Calcul local",
  "signature": "nearest(candidates, to)",
  "doc": "L'élément le plus proche de to (une unité ou (x,y)) ; renvoie None s'il n'y a aucun candidat."
 },
 {
  "name": "life_mana",
  "category": "observe",
  "status": "verified",
  "mechanism": "hp/hpMax/mana/manaMax des unités du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "life_mana(u) -> 'dict | None'",
  "doc": "{'hp','hp_max','mana','mana_max'} (flottants, valeurs brutes du moteur). Pour u, une unité issue de l'instantané suffit (elle est remplacée par la copie la plus récente)."
 },
 {
  "name": "hero_info",
  "category": "observe",
  "status": "verified",
  "mechanism": "level/xp/skillPoints des unités du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "hero_info(hero) -> 'dict | None'",
  "doc": "{'level','xp','skill_points'}."
 },
 {
  "name": "abilities",
  "category": "observe",
  "status": "verified",
  "mechanism": "Détails du bloc du monde : capacités (code/niveau/drapeaux/recharge restante)",
  "latency": "Instantané poussé",
  "signature": "abilities(u) -> 'list'",
  "doc": "[{code, level, cooldown, flags}] ; les buffs sont dans buffs(u). Disponible uniquement pour les unités « avec détails » (héros > unités des joueurs > creeps, 256 au maximum)."
 },
 {
  "name": "buffs",
  "category": "observe",
  "status": "verified",
  "mechanism": "Détails du bloc du monde : objets de capacité dont le code commence par B",
  "latency": "Instantané poussé",
  "signature": "buffs(u) -> 'list'",
  "doc": "Codes des buffs portés par l'unité (par exemple 'BHds' Bouclier divin, 'Bslo' Ralentissement). L'effet de chaque code est décrit dans data/game/buffs.json."
 },
 {
  "name": "cooldown",
  "category": "observe",
  "status": "verified",
  "mechanism": "Détails du bloc du monde : recharge restante des capacités (minuteur de capacité)",
  "latency": "Instantané poussé",
  "signature": "cooldown(u, ability: 'str') -> 'float | None'",
  "doc": "Secondes de recharge restantes pour cette capacité (secondes de jeu) ; 0 = utilisable ; renvoie None si l'unité n'a pas cette capacité (ou n'a pas de détails)."
 },
 {
  "name": "inventory",
  "category": "observe",
  "status": "verified",
  "mechanism": "Détails du bloc du monde : 6 emplacements d'inventaire",
  "latency": "Instantané poussé",
  "signature": "inventory(hero) -> 'list | None'",
  "doc": "Codes à quatre caractères des 6 emplacements d'objets (None pour un emplacement vide) ; renvoie None si l'unité n'a pas d'inventaire."
 },
 {
  "name": "current_order",
  "category": "observe",
  "status": "verified",
  "mechanism": "order / cible d'ordre / point cible d'ordre des unités du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "current_order(u) -> 'dict | None'",
  "doc": "{'order','target','x','y'} : l'ordre que l'unité est en train d'exécuter (order vaut 0x000D00xx ou le code à quatre caractères d'un bâtiment, 0 = inactive).\ntarget est une paire de handles ; convertissez-la en unité avec g.unit(target)."
 },
 {
  "name": "current_target",
  "category": "observe",
  "status": "verified",
  "mechanism": "Cible de tâche des unités du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "current_target(u)",
  "doc": "L'unité que cette unité **attaque ou poursuit réellement** (None s'il n'y en a pas).\n⚠ Après un ordre d'attaque, l'emplacement d'ordre se vide rapidement et l'attaque est portée par la tâche — pour savoir « qui elle attaque », utilisez cette méthode, pas current_order."
 },
 {
  "name": "clock",
  "category": "observe",
  "status": "verified",
  "mechanism": "clockMs de l'en-tête du bloc du monde (horloge de jeu du moteur)",
  "latency": "Instantané poussé",
  "signature": "clock() -> 'float | None'",
  "doc": "Horloge de jeu du moteur (en secondes de jeu, 0 pendant le chargement). À vitesse accélérée, elle avance plus vite que le temps réel."
 },
 {
  "name": "production",
  "category": "observe",
  "status": "verified",
  "mechanism": "Table de production du bloc du monde (objets de capacité Aque/ABnP/AUnP + temps écoulé suivi par le runtime ; écart mesuré < 0.2 seconde de jeu)",
  "latency": "Instantané poussé",
  "signature": "production(building)",
  "doc": "Ce que produit ce bâtiment : Production(kind, queue, duration, elapsed, blocked, progress, remaining…), ou None s'il ne produit rien.\n  kind 'queue' (entraînement/recherche/héros ; queue compte 7 emplacements au maximum, [0] est en cours) / 'construction' (en construction) / 'upgrade' (amélioration du hall/d'une tour) ;\n  blocked = en file mais pas démarré (le plus souvent nourriture insuffisante — il est temps de construire une ferme) ; progress 0..1.\nLes bâtiments adverses sont aussi consultables (en mode équitable, uniquement ceux qui sont visibles)."
 },
 {
  "name": "queue",
  "category": "observe",
  "status": "verified",
  "mechanism": "Table de production du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "queue(building) -> 'list'",
  "doc": "Codes à quatre caractères de la file d'entraînement/recherche ([0] en cours) ; [] si le bâtiment est inactif ou n'est pas un bâtiment de production."
 },
 {
  "name": "all_production",
  "category": "observe",
  "status": "verified",
  "mechanism": "Table de production du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "all_production(owner: 'str | int' = 'me') -> 'list'",
  "doc": "Toutes les productions en cours [(bâtiment, Production)]. owner comme pour units() : 'me' / 'enemy' / numéro de joueur / 'all'.\nUsage pro : voir quelles unités l'adversaire entraîne, quelles technologies il recherche, quand il passe de tier (quand vous repérez ses bâtiments)."
 },
 {
  "name": "path_distance",
  "category": "observe",
  "status": "verified",
  "mechanism": "Bloc de carte (IsTerrainPathable du moteur) + bloc des arbres + emprise des bâtiments, A* côté SDK (cases de 128)",
  "latency": "Instantané poussé + calcul local",
  "signature": "path_distance(a, b) -> 'float | None'",
  "doc": "Distance à parcourir par une unité terrestre de a à b (a et b : unités ou (x,y)) ; None si inaccessible. Sur les cartes à îles, utilisez-la pour savoir « si ce camp de creeps / cette expansion est accessible par voie terrestre » ;\nelle est plus fiable que la distance à vol d'oiseau (elle contourne forêts, falaises et bâtiments). Précision d'une case de 128 ; un passage plus étroit qu'une case est considéré comme bloqué."
 },
 {
  "name": "reachable",
  "category": "observe",
  "status": "verified",
  "mechanism": "Idem",
  "latency": "Instantané poussé + calcul local",
  "signature": "reachable(a, b) -> 'bool | None'",
  "doc": "Accessible ou non par voie terrestre (None si le bloc de carte n'est pas encore calculé)."
 },
 {
  "name": "walk_path",
  "category": "observe",
  "status": "verified",
  "mechanism": "Idem",
  "latency": "Instantané poussé + calcul local",
  "signature": "walk_path(a, b) -> 'list | None'",
  "doc": "Points d'inflexion du chemin [(x,y)...] (le dernier point est b) ; avec path(units, liste de points), faites suivre ce chemin à vos troupes (contourner les tours, emprunter des chemins détournés)."
 },
 {
  "name": "upkeep",
  "category": "observe",
  "status": "inferred",
  "mechanism": "Règle fixe de la 1.27 : de 0 à 50 de nourriture, pas d'entretien ; de 51 à 80, revenu ×0.7 ; de 81 à 100, ×0.4",
  "latency": "Instantané poussé",
  "signature": "upkeep(player: 'int | None' = None) -> 'dict | None'",
  "doc": "Palier d'entretien : {'level': 'none'/'low'/'high', 'income': 1.0/0.7/0.4, 'next_at': nourriture du palier suivant (aucun = None)}.\nRègle bien connue des pros : restez à 50 de nourriture pendant le passage au tier 3 et les améliorations d'attaque/armure, et ne montez à 80 qu'avant la bataille décisive."
 },
 {
  "name": "xp_to_next",
  "category": "observe",
  "status": "verified",
  "mechanism": "level/xp du bloc du monde + formule NeedHeroXP de MiscGame",
  "latency": "Instantané poussé",
  "signature": "xp_to_next(hero) -> 'int | None'",
  "doc": "Expérience qu'il manque au héros pour atteindre le niveau suivant (niveau 10 = 0)."
 },
 {
  "name": "creep_camps",
  "category": "observe",
  "status": "verified",
  "mechanism": "Instantané poussé (creeps regroupés dans un rayon de 600) + niveaux de units.json",
  "latency": "Instantané poussé",
  "signature": "creep_camps(link: 'float' = 600.0) -> 'list'",
  "doc": "Regroupe les creeps (visibles) en camps : [{'x','y','units','level','hp','max_level'}], du plus proche au plus éloigné de votre base principale.\nlevel = niveau total du camp (la mesure courante de la difficulté du creeping), hp = PV totaux. À combiner avec time_to_kill / path_distance pour choisir un camp."
 },
 {
  "name": "buff_info",
  "category": "observe",
  "status": "verified",
  "mechanism": "data/game/buffs.json (BuffID de AbilityData.slk -> capacité/effet/durée)",
  "latency": "Données locales",
  "signature": "buff_info(code: 'str') -> 'dict | None'",
  "doc": "Ce qu'est un code de buff : {'ability','effect','dur','hero_dur','targets'} (ex. 'Bslo' -> Ralentissement). Si un code a plusieurs lignes, la première est renvoyée."
 },
 {
  "name": "stats",
  "category": "observe",
  "status": "verified",
  "mechanism": "Tables de données (UnitBalance/UnitWeapons/UpgradeData/MiscGame) + niveaux de technologie en temps réel + niveau du héros",
  "latency": "Instantané poussé + voie rapide (un lot toutes les 5 s)",
  "signature": "stats(u, player: 'int | None' = None)",
  "doc": "Caractéristiques de combat de l'unité, combat.UnitStats : PV/mana max, armure (améliorations d'attaque/armure et agilité du héros comprises), type d'armure, vitesse de déplacement, vision de jour/de nuit,\narmes (ce qu'elles peuvent toucher, portée, intervalle d'attaque, plage de dégâts, type d'attaque, dégâts de zone). u : une unité (la technologie de son propriétaire et le niveau du héros sont appliqués automatiquement) ou un code à quatre caractères (player vaut par défaut votre joueur).\nÀ combiner avec .dps_vs(adversaire) / .hits_to_kill(adversaire) / combat.time_to_kill(groupe, adversaire). ⚠ Objets, auras et buffs non pris en compte."
 },
 {
  "name": "time_to_kill",
  "category": "observe",
  "status": "verified",
  "mechanism": "stats() + PV en temps réel",
  "latency": "Instantané poussé",
  "signature": "time_to_kill(attackers, target) -> 'float | None'",
  "doc": "Secondes de jeu nécessaires à ce groupe d'unités pour tuer target ensemble (selon les PV actuels de target ; contres, armure et améliorations d'attaque/armure pris en compte ; déplacements, dégâts de zone et soins ignorés).\nUsage pro : en tir concentré, frappez d'abord l'unité « qui meurt le plus vite » (time_to_kill minimal), pas la plus proche. Impossible à atteindre = None."
 },
 {
  "name": "time_of_day",
  "category": "observe",
  "status": "verified",
  "mechanism": "Zone d'extension du bloc du monde : GetFloatGameState(GAME_STATE_TIME_OF_DAY)",
  "latency": "Instantané poussé",
  "signature": "time_of_day() -> 'float | None'",
  "doc": "Heure du jour dans le jeu (en heures, 0~24). La partie commence à 8 h du matin ; une journée complète = 480 secondes de jeu (240 s de jour et 240 s de nuit, mises à l'échelle par la vitesse du cycle jour/nuit).\nRenvoie None si la lecture échoue (ancien runtime / hors partie)."
 },
 {
  "name": "is_night",
  "category": "observe",
  "status": "verified",
  "mechanism": "Zone d'extension du bloc du monde (jour de 6 h à 18 h)",
  "latency": "Instantané poussé",
  "signature": "is_night() -> 'bool | None'",
  "doc": "Indique s'il fait nuit (18:00~6:00). Usage pro : la nuit, les creeps dorment (en frappant le premier, vous n'êtes pas encerclé), la vision de toutes les unités diminue (bon moment pour une attaque surprise),\net les sentinelles/unités des Elfes de la nuit deviennent invisibles près des arbres. Renvoie None si la lecture échoue."
 },
 {
  "name": "seconds_until",
  "category": "observe",
  "status": "verified",
  "mechanism": "Zone d'extension du bloc du monde + journée de 480 secondes (mesuré : 20 secondes de jeu par heure)",
  "latency": "Instantané poussé",
  "signature": "seconds_until(hour: 'float') -> 'float | None'",
  "doc": "Secondes de jeu restantes avant qu'il soit hour heures dans le jeu (par exemple seconds_until(18) = temps restant avant la tombée de la nuit, pour planifier un creeping nocturne)."
 },
 {
  "name": "items_on_ground",
  "category": "observe",
  "status": "verified",
  "mechanism": "items[] du bloc du monde (uniquement les objets au sol : handle du porteur entièrement à FF)",
  "latency": "Instantané poussé",
  "signature": "items_on_ground() -> 'list'",
  "doc": "Objets au sol [Item(addr, handle_lo, handle_hi, type, x, y, life)]. Ramasser ou utiliser un objet émet l'événement item.removed."
 },
 {
  "name": "trees",
  "category": "observe",
  "status": "verified",
  "mechanism": "Bloc des arbres Local\\War3Trees_<pid> (rafraîchi toutes les 2 secondes)",
  "latency": "Instantané poussé",
  "signature": "trees(x: 'float | None' = None, y: 'float | None' = None, limit: 'int' = 60) -> 'list'",
  "doc": "Arbres vivants (ceux dont le targType dans DestructableData contient tree) ; si (x,y) est fourni, triés du plus proche au plus éloigné, limit arbres au maximum.\nChaque arbre est un Tree(addr, handle_lo, handle_hi, type, x, y, life), que vous pouvez passer directement à gather pour couper du bois."
 },
 {
  "name": "events",
  "category": "observe",
  "status": "verified",
  "mechanism": "Anneau d'événements Local\\War3Events_<pid> (comparaison des publications + événements de dégâts capturés par le runtime)",
  "latency": "Instantané poussé",
  "signature": "events() -> 'list'",
  "doc": "Ce qui s'est passé depuis le dernier appel : unit.appeared / unit.died / unit.removed / unit.damaged / order.changed /\nhero.levelup / owner.changed / item.appeared / item.removed / game.started (obtenus en comparant les publications, précision = période de publication de 50 ms),\nainsi que les événements de niveau moteur damage / killed (le runtime les enregistre sur le thread du jeu au moment où ils se produisent : il y en a un pour **chaque coup**) :\n    damage : handle = l'unité touchée, .source_addr = celle qui frappe (convertissez-la en unité avec snapshot().unit_by_addr), .value = PV réellement perdus,\n            .raw_damage = dégâts avant armure, .attack_type (normal/pierce/siege/magic/chaos/hero/spell), .damage_type\n    killed : ce coup a tué l'unité, .source_addr = le tueur\nainsi que production.done, obtenu par le suivi de la table de production par le runtime (précision = période de publication) : unité = le bâtiment, .done_code = code à quatre caractères de ce qui est terminé,\n    .done_kind = 'training' (unités/héros/résurrection) / 'research' / 'construction' (bâtiment terminé) / 'upgrade' (passage de tier / amélioration de tour), .value = secondes de jeu nécessaires\nAjouts du 09-25 :\n    spell.cast : unité = le lanceur, .spell code à quatre caractères du sort, b niveau, value recharge en secondes, x,y point d'incantation (détecté quand la recharge du sort commence, précision = période de publication)\n    player.left : .player numéro du joueur parti / retiré après sa défaite ; game.ended : on quitte la partie\n    selection.changed : la sélection du joueur local a changé (g.selection() pour obtenir les unités)\n    message : une ligne d'un cadre de messages à l'écran (indication du jeu, chat, système) : .text texte intégral, .frame numéro du cadre de messages,\n             .chat = {'channel', 'sender', 'text'} (quand c'est du chat ; c'est là qu'on lit ce que le joueur tape dans la boîte de chat)\n    ui.click / ui.hover / hotkey / mouse.world : interface et entrées (g.ui), .key est la key du canevas / la syntaxe du raccourci\nChaque entrée est un Event(seq, kind, clock, addr, handle, type, owner, a, b, x, y, value, extra).\nEn mode équitable (fair=True), seuls sont fournis : les événements de vos propres unités, ceux des unités visibles en ce moment (ou encore visibles dans la dernière seconde), les dégâts subis ou infligés par votre camp,\nainsi que les événements locaux d'interface / de messages / de partie."
 },
 {
  "name": "selection",
  "category": "observe",
  "status": "verified",
  "mechanism": "Zone d'extension du bloc du monde W3P, selAddrs (à chaque publication, le runtime y joint la sélection du joueur local)",
  "latency": "Instantané poussé",
  "signature": "selection() -> 'list'",
  "doc": "Unités actuellement sélectionnées par le joueur local (l'unité principale en premier ; 12 au maximum). Tout changement de sélection émet l'événement selection.changed."
 },
 {
  "name": "messages",
  "category": "observe",
  "status": "verified",
  "mechanism": "Mémoire partagée Local\\War3Msgs_<pid> (messages à l'écran capturés par le runtime)",
  "latency": "Instantané poussé",
  "signature": "messages() -> 'list'",
  "doc": "Messages apparus dans les cadres de messages à l'écran depuis le dernier appel : [{'text', 'frame', 'repeat', 'seq', 'game_ms'}].\nLes indications du jeu (« Il vous faut plus de fermes », « Impossible de construire ici »), le chat et les messages système s'y trouvent tous ; frame indique de quel cadre de messages il s'agit.\nCe sont les mêmes messages que les événements message du flux d'événements (chacun avec son propre curseur)."
 },
 {
  "name": "ui",
  "category": "control",
  "status": "verified",
  "mechanism": "W3P 74 input_enable + mémoire partagée Local\\War3Input_<pid> (le runtime reçoit les entrées de la fenêtre)",
  "latency": "Mémoire partagée",
  "signature": "ui()",
  "doc": "Interface et entrées (openwar3.ui.UI) : boutons et cartes de choix cliquables, raccourcis clavier, choix d'une position par un clic au sol, ce que pointe la souris.\nLe jeu ne reçoit pas le clic qui tombe sur un bouton ; uniquement de l'entrée locale + du dessin local, donc sûr même en multijoueur."
 },
 {
  "name": "tech",
  "category": "observe",
  "status": "verified",
  "mechanism": "Requête W3P q_tech (décompte des technologies du joueur par le moteur)",
  "latency": "Voie rapide",
  "signature": "tech(code: 'str', player: 'int | None' = None) -> 'int | None'",
  "doc": "Niveau de recherche / nombre de bâtiments terminés (les chaînes d'amélioration comptent : un château compte aussi comme htow). player vaut par défaut votre joueur ; n'importe quel joueur peut être interrogé."
 },
 {
  "name": "can_do",
  "category": "observe",
  "status": "verified",
  "mechanism": "Requête W3P q_feasible (vérification de faisabilité du moteur)",
  "latency": "Voie rapide",
  "signature": "can_do(u, code: 'str') -> 'int | None'",
  "doc": "Verdict de faisabilité du moteur : 0/220 possible ; 3 nourriture, 8 or manquant, 9 bois manquant, 32 file pleine, 183 prérequis manquant, 185 autel en cours de résurrection, 221 élément absent/en construction.\n⚠ Vaut toujours 221 pour un ouvrier qui construit un bâtiment : inutilisable pour valider un emplacement (utilisez build_near)."
 },
 {
  "name": "can_do_many",
  "category": "observe",
  "status": "verified",
  "mechanism": "Requête W3P q_feasible × N, soumise en un lot",
  "latency": "Voie rapide × 1",
  "signature": "can_do_many(pairs) -> 'list'",
  "doc": "Pose de nombreuses questions can_do d'un coup : pairs = [(unité, code à quatre caractères), ...], renvoie la liste des codes de verdict dans le même ordre (None pour une question sans réponse).\nPour planifier ce qu'un tick doit construire/entraîner, interrogez d'abord tout en bloc : c'est N fois plus rapide que des can_do un par un (cerveau de référence, 09-23 : planification des constructions 76 -> 25 ms)."
 },
 {
  "name": "tech_many",
  "category": "observe",
  "status": "verified",
  "mechanism": "Requête W3P q_tech × N, soumise en un lot",
  "latency": "Voie rapide × 1",
  "signature": "tech_many(codes, player: 'int | None' = None) -> 'dict'",
  "doc": "Interroge d'un coup de nombreux décomptes de technologies/bâtiments : {code à quatre caractères: nombre ou None}."
 },
 {
  "name": "visible",
  "category": "observe",
  "status": "verified",
  "mechanism": "Requête W3P q_visible (visible / brouillard / masque noir)",
  "latency": "Voie rapide",
  "signature": "visible(x: 'float', y: 'float') -> 'bool | None'",
  "doc": "Indique si ce point est visible par votre camp en ce moment (ni dans le brouillard ni dans le masque noir). Un bot en mode équitable ne devrait utiliser que les ennemis visibles."
 },
 {
  "name": "gold_left",
  "category": "observe",
  "status": "inferred",
  "mechanism": "Requête W3P q_mine_gold (or restant dans la mine selon le moteur)",
  "latency": "Voie rapide",
  "signature": "gold_left(mine) -> 'int | None'",
  "doc": "Or restant dans la mine d'or."
 },
 {
  "name": "enemy_ai_plan",
  "category": "observe",
  "status": "verified",
  "mechanism": "Requête W3P q_captain (le capitaine de l'ordinateur que suivent les unités ennemies)",
  "latency": "Voie rapide",
  "signature": "enemy_ai_plan(enemy_unit) -> 'dict | None'",
  "doc": "Le capitaine de l'IA de l'ordinateur : où il emmène ses troupes (vous savez avant même son départ quelle partie de votre base il va attaquer). Fonctionne uniquement contre un adversaire ordinateur ; renvoie None si l'unité ne suit pas de capitaine."
 },
 {
  "name": "batch",
  "category": "command",
  "status": "verified",
  "mechanism": "Les commandes du bloc sont accumulées en un lot, soumis en une fois à la fin du bloc (exécuté dans la même frame, une seule attente du thread du jeu)",
  "latency": "Voie rapide × 1",
  "signature": "batch() -> 'Batch'",
  "doc": "Regroupe les commandes d'un tick en un lot :\n\n    with g.batch() as b:\n        g.attack(archers, target)          # renvoie Pending, qui ne devient un reçu qu'à la fin du bloc\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\nEnvoyée seule, chaque commande attend un passage du thread du jeu (environ 10 ms) ; un lot n'attend qu'une fois — c'est ainsi que le cerveau de référence (09-23) est passé de 48 -> 26 ms par cycle.\n* L'arbitrage s'applique toujours commande par commande (une unité réservée reçoit immédiatement un reçu held et n'entre pas dans le lot) ;\n* Dans le bloc, les commandes renvoient Pending : lire son .ok avant la fin du bloc lève une erreur (le reçu n'existe pas encore) ; après la fin du bloc, il s'utilise comme un Receipt ;\n* Une exception levée dans le bloc = tout le lot est annulé (status 97 cancelled), et les unités réservées sont libérées ;\n* Les requêtes (can_do / tech / visible …), build_near et buy n'entrent pas dans le lot et restent posées sur-le-champ — leur résultat est nécessaire immédiatement ;\n  pour poser de nombreuses questions d'un coup, utilisez can_do_many / tech_many ;\n* Un with g.batch() imbriqué est fusionné dans le lot le plus externe ; au-delà de 16 commandes, le runtime découpe automatiquement en plusieurs segments (une attente par segment)."
 },
 {
  "name": "order_of",
  "category": "observe",
  "status": "verified",
  "mechanism": "Ordre de l'instantané + commandes de ce processus qui viennent d'être acceptées (reçus)",
  "latency": "Instantané poussé",
  "signature": "order_of(u) -> 'int | None'",
  "doc": "L'ordre actuel de l'unité, **y compris celui que vous venez de donner pendant ce tick** (tant que l'instantané n'a pas rattrapé, le nouvel ordre du reçu est utilisé).\n⚠ Constaté en partie réelle le 09-23 : hello_bot venait d'envoyer un paysan construire une ferme ; au même tick, rush_bot le voyait « inactif » dans l'instantané et l'envoyait construire une caserne, si bien que la ferme était abandonnée à mi-chemin, encore et encore.\n  Pour choisir des unités « inactives / qui ne construisent pas », utilisez cette méthode plutôt que u.order."
 },
 {
  "name": "move",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P point : move (bit extra = mode de mise en file)",
  "latency": "Voie rapide",
  "signature": "move(units, x: 'float', y: 'float', queue: 'str | None' = None)",
  "doc": "Se rend en (x,y) sans attaquer en chemin (à utiliser pour battre en retraite). Accepte une unité ou une liste (l'ordre est donné dans la même frame).\nqueue='after' : termine d'abord la tâche en cours (inséré après l'ordre actuel). values[0] du reçu = nombre d'ordres en file pour cette unité après l'ordre (celui en cours compris)."
 },
 {
  "name": "attack_move",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P point : attack sur un point",
  "latency": "Voie rapide",
  "signature": "attack_move(units, x: 'float', y: 'float', queue: 'str | None' = None)",
  "doc": "Attaque-déplacement (A + clic au sol) : attaque les ennemis rencontrés en chemin. queue comme pour move."
 },
 {
  "name": "attack",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target : commande sur cible (clic droit smart)",
  "latency": "Voie rapide",
  "signature": "attack(units, target, force: 'bool' = False, queue: 'str | None' = None)",
  "doc": "Attaque target. Utilise par défaut le clic droit (sur un ennemi = attaquer celui-ci ; mesuré le 09-23 : la cible d'ordre et la cible de tâche sont toutes deux cette unité).\n⚠ La cible doit être dans le champ de vision ; une cible invisible est rejetée (code de motif 1001).\nforce=True utilise l'ordre d'attaque 0x0F (nécessaire pour attaquer une unité alliée / un petit animal neutre) — mesuré : il ne fait que changer l'ordre en attaque sans mémoriser la cible,\net l'unité part attaquer d'autres ennemis à proximité ; ne l'utilisez pas pour une cible précise."
 },
 {
  "name": "stop",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P immediate : stop",
  "latency": "Voie rapide",
  "signature": "stop(units)",
  "doc": "Arrête tout (ordre 0x000D0004) et vide aussi les ordres en file."
 },
 {
  "name": "hold",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P immediate : holdposition",
  "latency": "Voie rapide",
  "signature": "hold(units, queue: 'str | None' = None)",
  "doc": "Tenir la position (ne poursuit pas, n'attaque que ce qui est à portée)."
 },
 {
  "name": "patrol",
  "category": "command",
  "status": "inferred",
  "mechanism": "W3P point : patrol",
  "latency": "Voie rapide",
  "signature": "patrol(units, x: 'float', y: 'float', queue: 'str | None' = None)",
  "doc": "Patrouille entre la position actuelle et (x,y)."
 },
 {
  "name": "attack_ground",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P point : attackground (unités de siège / mortiers / catapultes)",
  "latency": "Voie rapide",
  "signature": "attack_ground(units, x: 'float', y: 'float', queue: 'str | None' = None)",
  "doc": "Attaque au sol : l'artillerie tire sur une zone (unités invisibles, ennemis derrière des arbres, blocage d'un passage). Seules les unités capables d'attaquer le sol acceptent l'ordre."
 },
 {
  "name": "cancel",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P immediate : cancel",
  "latency": "Voie rapide",
  "signature": "cancel(building)",
  "doc": "Annuler : le dernier emplacement de la file d'entraînement/recherche (remboursé), un bâtiment en construction (75 % remboursés), un hall en cours d'amélioration."
 },
 {
  "name": "path",
  "category": "command",
  "status": "verified",
  "mechanism": "Un lot : le premier segment s'exécute immédiatement, les autres sont insérés en ordre inverse avec queue='after' (le moteur ne sait insérer qu'après l'ordre en cours)",
  "latency": "Voie rapide × 1",
  "signature": "path(units, points, attack: 'bool' = False)",
  "doc": "Parcourt une suite de points dans l'ordre (points enchaînés avec Shift : points de passage, contournement de tours, itinéraire d'éclaireur). attack=True fait de chaque segment une attaque-déplacement.\nSoumis en une fois ; un reçu par point (dans l'ordre de points)."
 },
 {
  "name": "gather",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target : harvest (mine d'or ou arbre)",
  "latency": "Voie rapide",
  "signature": "gather(workers, target, queue: 'str | None' = None)",
  "doc": "Récolte de l'or / coupe du bois (target est une mine d'or ou un arbre de trees()). ⚠ N'affectez que des ouvriers inactifs (idle_workers) : redonner l'ordre à un ouvrier qui a une tâche interrompt son cycle de récolte.\nUsage pro : retourner à la mine après une construction = gather(worker, mine, queue='after') après build(...)."
 },
 {
  "name": "repair",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target : repair",
  "latency": "Voie rapide",
  "signature": "repair(workers, building, queue: 'str | None' = None)",
  "doc": "Réparer / aider à construire (un chantier humain ou orc s'arrête si personne n'y travaille)."
 },
 {
  "name": "build",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P build : ordre de construction, confirmé en relisant l'ordre de l'ouvrier dans la même frame",
  "latency": "Voie rapide",
  "signature": "build(worker, code: 'str', x: 'float', y: 'float', queue: 'str | None' = None)",
  "doc": "Ordonne à un ouvrier de construire code en (x,y) (coordonnées alignées sur une grille de 32). Reçu accepté = l'ordre de l'ouvrier est déjà ce bâtiment (ou l'ordre de démarrage du chantier) ;\navec queue='after' = ajouté à la file d'ordres de l'ouvrier (values[0] du reçu = nombre en file).\n⚠ Accepté ≠ construit : le moteur accepte aussi sur le moment un emplacement en pleine forêt, et l'ouvrier n'échoue qu'une fois sur place (mesuré le 09-23) ; si l'argent est dépensé ailleurs entre-temps, les fondations n'apparaissent pas non plus.\nSi vous ne savez pas où le bâtiment peut être placé, utilisez build_near (il suit le résultat et met sur liste noire les emplacements en échec). Pour en enchaîner plusieurs, utilisez build_queue."
 },
 {
  "name": "build_queue",
  "category": "command",
  "status": "verified",
  "mechanism": "Un lot : le premier immédiatement, les autres en ordre inverse avec queue='after'",
  "latency": "Voie rapide × 1",
  "signature": "build_queue(worker, plan)",
  "doc": "Un ouvrier construit plusieurs bâtiments d'affilée (construction enchaînée avec Shift) : plan = [(code à quatre caractères, x, y), ...]. Soumis en une fois ; reçus dans l'ordre de plan.\n⚠ L'argent n'est prélevé qu'au début de chaque construction (pas lors de la mise en file) — avec 3 bâtiments en file mais de l'argent pour un seul, les deux suivants échouent quand l'ouvrier arrive sur place."
 },
 {
  "name": "build_near",
  "category": "command",
  "status": "verified",
  "mechanism": "build point par point + suivi (fondations apparues = réussite ; ouvrier qui abandonne l'ordre sans fondations = emplacement mis sur liste noire)",
  "latency": "Voie rapide × nombre de points essayés",
  "signature": "build_near(worker, code: 'str', x: 'float', y: 'float', min_r: 'float' = 450, max_r: 'float' = 1500, max_tries: 'int' = 24)",
  "doc": "Cherche autour de (x,y), du plus proche au plus éloigné, un emplacement libre pour construire code. **Non bloquant**, peut être appelé à chaque tick :\n  * une construction de ce type est encore en cours (l'ouvrier est en chemin) -> renvoie cet emplacement, sans redonner l'ordre ;\n  * la précédente a réussi (les fondations sont apparues) -> cherche au besoin un nouvel emplacement ;\n  * la précédente a échoué (l'ouvrier a découvert sur place que le bâtiment ne pouvait pas être posé, le moteur a retiré l'ordre, aucune fondation) -> cet emplacement est mis sur liste noire pendant 45 secondes, on passe au suivant ;\n  * argent insuffisant -> renvoie directement None (sans essayer ni mettre sur liste noire) ; si tous les emplacements ont été essayés, renvoie None.\n⚠ Pourquoi ce suivi : constaté en partie réelle le 09-23, le moteur **accepte sur le moment** un emplacement en pleine forêt, et l'ouvrier n'échoue qu'une fois sur place (le reçu de la même frame ne permet pas de le détecter) ;\n  de plus, la vérification d'emplacement du moteur renvoie toujours 221 pour un ouvrier qui construit, on ne peut donc pas « vérifier » avant de construire. Seul un emplacement manifestement occupé (le centre du hall) est rejeté sur-le-champ."
 },
 {
  "name": "can_afford",
  "category": "observe",
  "status": "verified",
  "mechanism": "Ressources de votre camp dans l'instantané poussé + prix de units.json",
  "latency": "Instantané poussé",
  "signature": "can_afford(code: 'str') -> 'bool'",
  "doc": "Indique si l'or et le bois actuels suffisent pour acheter code (unité, bâtiment ; selon les prix de units.json). Tout ce qui est absent de la table des prix est considéré comme abordable.\n⚠ Pour les codes de passage de tier, la table contient le prix cumulé : le résultat est donc prudent ; c'est le reçu du moteur qui fait foi."
 },
 {
  "name": "train",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P immediate : code à quatre caractères, avec le code de motif de faisabilité en cas de rejet",
  "latency": "Voie rapide",
  "signature": "train(building, code: 'str')",
  "doc": "Entraîne une unité / recherche une technologie / améliore le hall (passer de tier = donner au hall lui-même le code à quatre caractères du hall cible, par exemple 'hkee').\nEn cas de rejet, le reason du reçu en donne la raison (nourriture insuffisante, or manquant, bois manquant, file pleine, prérequis manquant…)."
 },
 {
  "name": "learn",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P learn : la compétence n'est considérée comme apprise que si les points de compétence diminuent",
  "latency": "Voie rapide",
  "signature": "learn(hero, ability: 'str')",
  "doc": "Le héros apprend une compétence (code à quatre caractères, par exemple 'AHbz' Blizzard)."
 },
 {
  "name": "cast",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target / point / immediate (choisi selon les paramètres)",
  "latency": "Voie rapide",
  "signature": "cast(u, spell, target=None, x: 'float | None' = None, y: 'float | None' = None)",
  "doc": "Lance un sort. spell est le nom d'ordre ('thunderbolt' Éclair de tempête, 'blizzard', 'holybolt' Lumière sacrée…, voir data/order-ids.txt) ou l'identifiant d'ordre.\nAvec target = sur une unité ; avec x,y = sur le sol ; sans l'un ni l'autre = sans cible (Coup de tonnerre, Bouclier divin, Invocation d'élémentaire d'eau).\nUn reçu accepté signifie seulement que le moteur a pris l'ordre ; pour savoir si le sort est vraiment parti, vérifiez que cooldown() indique une recharge ou que le buff apparaît dans buffs()."
 },
 {
  "name": "rally",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P rally",
  "latency": "Voie rapide",
  "signature": "rally(building, x: 'float | None' = None, y: 'float | None' = None, target=None)",
  "doc": "Définit le point de ralliement (sur un point, ou sur une unité / une mine d'or)."
 },
 {
  "name": "revive",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P revive : liste des héros morts -> l'autel lance la résurrection sur le héros mort",
  "latency": "Voie rapide",
  "signature": "revive(altar, hero=None)",
  "doc": "Ressuscite à l'autel un héros mort (sans hero, le premier de la liste de résurrection).\nCauses de rejet fréquentes (indiquées dans le reason du reçu) : nourriture insuffisante (les héros consomment aussi de la nourriture), argent insuffisant, mort trop récente (résurrection possible environ 3 secondes de jeu après la mort),\nrésurrection déjà en cours (le moteur libère cet emplacement sur-le-champ quand il accepte l'ordre)."
 },
 {
  "name": "pick_up",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target : clic droit sur l'objet",
  "latency": "Voie rapide",
  "signature": "pick_up(hero, item)",
  "doc": "Le héros va ramasser un objet au sol (item provient de items_on_ground). Une fois ramassé, l'objet apparaît dans l'inventaire et l'événement item.removed est émis pour le sol."
 },
 {
  "name": "use_item",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P use_item (par numéro d'emplacement)",
  "latency": "Voie rapide",
  "signature": "use_item(hero, slot: 'int', target=None, x: 'float | None' = None, y: 'float | None' = None)",
  "doc": "Utilise l'objet de l'emplacement slot (0~5) de l'inventaire ; peut prendre une unité cible ou un point cible.\n⚠ Pour un objet utilisé sur un point (par exemple une tour d'ivoire), le moteur renvoie 0 même en cas de succès, et le reçu est toujours compté comme accepté — vérifiez si l'emplacement d'inventaire s'est vidé."
 },
 {
  "name": "drop_item",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P item_drop (calqué sur JASS UnitDropItemPoint : dropitem 0xD0021 sur un point + objet en cible immédiate)",
  "latency": "Voie rapide",
  "signature": "drop_item(hero, slot: 'int', x: 'float', y: 'float')",
  "doc": "Dépose en (x,y) l'objet de l'emplacement slot de l'inventaire (le héros s'y rend pour le poser)."
 },
 {
  "name": "give_item",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P item_drop (calqué sur JASS UnitDropItemTarget : dropitem sur une unité)",
  "latency": "Voie rapide",
  "signature": "give_item(hero, slot: 'int', to)",
  "doc": "Donne l'objet de l'emplacement slot à to (un autre héros / une unité ; le héros se déplace pour le remettre). Donner à une boutique = vendre (voir sell_item)."
 },
 {
  "name": "sell_item",
  "category": "command",
  "status": "verified",
  "mechanism": "Comme give_item, avec la boutique pour cible (mesuré : un Staff of Sanctuary se vend 125 d'or)",
  "latency": "Voie rapide",
  "signature": "sell_item(hero, slot: 'int', shop)",
  "doc": "Vend à une boutique l'objet de l'emplacement slot de l'inventaire (le héros doit se rendre près de la boutique ; seuls les objets vendables sont acceptés, repris à moitié prix)."
 },
 {
  "name": "move_item",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P target : ordre 0xD0022+numéro d'emplacement, cible = l'objet (calqué sur JASS UnitDropItemSlot)",
  "latency": "Voie rapide",
  "signature": "move_item(hero, slot: 'int', to_slot: 'int')",
  "doc": "Change un objet d'emplacement dans l'inventaire (de l'emplacement slot vers to_slot ; si les deux sont occupés, ils sont échangés). Utile pour organiser les raccourcis clavier."
 },
 {
  "name": "buy",
  "category": "command",
  "status": "inferred",
  "mechanism": "W3P buy : la boutique vend au héros voisin",
  "latency": "Voie rapide",
  "signature": "buy(shop, item_code: 'str')",
  "doc": "Achète un objet dans une boutique (pour le héros qui se tient à côté). S'il manque un prérequis technologique, le moteur renvoie 0 et ne prélève rien."
 },
 {
  "name": "call_to_arms",
  "category": "command",
  "status": "verified",
  "mechanism": "W3P immediate : townbellon/off",
  "latency": "Voie rapide",
  "signature": "call_to_arms(hall, on: 'bool' = True)",
  "doc": "Appel aux armes des Humains : les paysans deviennent des miliciens (l'hôtel de ville de tier 1 n'a pas cette capacité ; valable uniquement pour le donjon/château)."
 },
 {
  "name": "set_speed",
  "category": "control",
  "status": "verified",
  "mechanism": "Action 47 (25~800 %)",
  "latency": "Canal de contrôle",
  "signature": "set_speed(percent: 'int') -> 'bool'",
  "doc": "Vitesse de jeu (100 = vitesse normale)."
 },
 {
  "name": "pause",
  "category": "control",
  "status": "verified",
  "mechanism": "W3P pause",
  "latency": "Voie rapide",
  "signature": "pause(on: 'bool' = True)",
  "doc": "Met en pause / reprend la partie. Pendant la pause, l'horloge du moteur s'arrête, mais la voie rapide accepte toujours les ordres (la distribution d'événements continue de tourner)."
 },
 {
  "name": "set_publish_period",
  "category": "control",
  "status": "verified",
  "mechanism": "requestedPeriodMs du bloc du monde",
  "latency": "Instantané poussé",
  "signature": "set_publish_period(ms: 'int') -> 'None'",
  "doc": "Période de publication de l'état du monde (16~1000 millisecondes, 50 par défaut). Un relevé prend environ 0.5 ms, 33 ms ne pose donc aucun problème ; la valeur est partagée par toute la machine, la dernière écrite l'emporte."
 },
 {
  "name": "say",
  "category": "control",
  "status": "verified",
  "mechanism": "Action 56",
  "latency": "Canal de contrôle",
  "signature": "say(u, text: 'str', seconds: 'float' = 4.0) -> 'bool'",
  "doc": "Affiche une bulle de dialogue au-dessus d'une unité (pour le streaming / le débogage, sans effet sur la partie). Renvoie False si la bulle n'est pas apparue ; la raison est dans g.last_say_error."
 },
 {
  "name": "message",
  "category": "control",
  "status": "inferred",
  "mechanism": "Action 45",
  "latency": "Canal de contrôle",
  "signature": "message(text: 'str') -> 'bool'",
  "doc": "Affiche une ligne dans la zone de messages en bas à gauche de l'écran de jeu (visible uniquement sur cette machine). Il faut attendre que le jeu ait lui-même affiché une notification (la DLL récupère la zone de messages à ce moment-là)."
 },
 {
  "name": "end_game",
  "category": "control",
  "status": "verified",
  "mechanism": "Action 22",
  "latency": "Canal de contrôle",
  "signature": "end_game() -> 'bool'",
  "doc": "Termine ce processus de jeu (farm.py --keep lance automatiquement la partie suivante d'après next_game.json)."
 },
 {
  "name": "canvas",
  "category": "control",
  "status": "verified",
  "mechanism": "W3P 73 canvas_enable + mémoire partagée Local\\War3Canvas_<pid> (dessinée par le runtime à chaque frame, juste avant que le jeu ne dessine le curseur de la souris ; le curseur passe par-dessus)",
  "latency": "Mémoire partagée",
  "signature": "canvas()",
  "doc": "Canevas : dessine sur l'écran de jeu des zones de texte, panneaux, barres de progression, images, cercles au sol et itinéraires (openwar3.canvas.Canvas).\nLe runtime dessine lui-même, sans créer de handle de jeu ni modifier l'état du jeu — sûr même en multijoueur ; style libre (caractères chinois, coins arrondis, transparence)."
 },
 {
  "name": "press_to_continue",
  "category": "control",
  "status": "verified",
  "mechanism": "PostMessage WM_KEYDOWN/UP de la touche Espace vers la fenêtre du jeu (sans lui donner le focus)",
  "latency": "Message de fenêtre",
  "signature": "press_to_continue() -> 'bool'",
  "doc": "Appuie une fois sur Espace sur l'écran de chargement « Appuyez sur une touche pour continuer ». Beaucoup de cartes RPG / scénarisées attendent une touche à la fin du chargement pour démarrer (mesuré le 09-24 sur WarChasers :\nsans appui, le jeu reste sur l'écran de chargement, horloge de jeu à 0, voie rapide jamais vidée). openwar3.run appuie de lui-même en attendant l'entrée en partie ; en général, inutile de l'appeler à la main."
 },
 {
  "name": "map_data",
  "category": "observe",
  "status": "verified",
  "mechanism": "Fichier de carte (chemin --map du lanceur) : w3u/w3t/w3a + wts ; pour une carte protégée, lit les TXT contenus dans la carte",
  "latency": "Lecture de fichier (~0.1 s la première fois)",
  "signature": "map_data()",
  "doc": "Données de la carte en cours (openwar3.mapdata.MapData) : name_of('HC07') pour le nom d'une unité / d'un objet / d'une capacité personnalisés, hero_names, tooltip.\nDans les cartes RPG, la plupart des unités sont créées par la carte elle-même et absentes de la table de noms intégrée ; si la partie n'a pas été lancée par le lanceur (fichier de carte introuvable), renvoie None."
 },
 {
  "name": "jass",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "W3P 70 jass (le runtime cherche la native par son nom dans sa table de 1291 natives)",
  "latency": "Voie rapide",
  "signature": "jass()",
  "doc": "Appelle n'importe quelle native JASS par son nom : g.jass.CreateUnit(g.jass.Player(1), \"Hpal\", x, y, 270.0).\nParamètres I/R/B/S/H convertis automatiquement (unités et objets se passent directement) ; en multijoueur, seules les natives en lecture seule peuvent être appelées. Détails dans openwar3/jass.py et docs/COMPANION_ZH.md."
 },
 {
  "name": "player_slots",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "JASS GetPlayerController / GetPlayerSlotState / IsPlayerAlly",
  "latency": "Voie rapide",
  "signature": "player_slots() -> 'list[dict]'",
  "doc": "Les 16 emplacements de joueur : controller (user = humain / computer / neutral…), state (empty / playing / left), human, me, ally (allié ou non de votre joueur).\nSert dans les cartes RPG à trouver un emplacement libre pour le compagnon, et à savoir si la partie est en solo."
 },
 {
  "name": "spawn",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "JASS CreateUnit + W3P 72 handle -> unité",
  "latency": "Voie rapide",
  "signature": "spawn(code: 'str', x: 'float', y: 'float', player: 'int | None' = None, facing: 'float' = 270.0)",
  "doc": "Crée une unité en (x,y) (player vaut par défaut le joueur local) et renvoie l'unité de l'instantané (après la publication suivante du monde, environ 50 ms) ; renvoie None si la création échoue.\nL'unité renvoyée a un attribut supplémentaire, jass_handle. ⚠ Uniquement en partie solo (en multijoueur, cela provoque une désynchronisation)."
 },
 {
  "name": "set_alliance",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "JASS SetPlayerAlliance",
  "latency": "Voie rapide",
  "signature": "set_alliance(a: 'int', b: 'int', allied: 'bool' = True, vision: 'bool' = True, control: 'bool' = False, xp: 'bool' = False, both: 'bool' = True) -> 'None'",
  "doc": "Définit la relation d'alliance du joueur a envers b : allied = pas d'attaque mutuelle + demande d'aide mutuelle ; vision = vision partagée ; control = contrôle partagé des unités (b peut commander les unités de a) ;\nxp = expérience partagée. both=True règle les deux sens à la fois (control seulement de a -> b)."
 },
 {
  "name": "set_player_name",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "JASS SetPlayerName",
  "latency": "Voie rapide",
  "signature": "set_player_name(player: 'int', name: 'str') -> 'None'",
  "doc": "Change le nom d'un joueur (celui affiché dans le tableau des scores, le chat et le panneau des alliances). Sert à donner un nom au compagnon."
 },
 {
  "name": "show_text",
  "category": "sandbox",
  "status": "verified",
  "mechanism": "JASS DisplayTimedTextToPlayer",
  "latency": "Voie rapide",
  "signature": "show_text(text: 'str', seconds: 'float' = 6.0, player: 'int | None' = None) -> 'None'",
  "doc": "Affiche une ligne de texte en bas à gauche de l'écran (le texte qu'utilisent les déclencheurs des cartes), par défaut pour le joueur local. Prend en charge les codes couleur |cffRRGGBB."
 }
]