Files
le-mystere-professeur-zacus/frontend-v3/packages/shared/blockly/zacus_blocks.mjs
T
clemsail 56c44eaf4f
Repo State / repo-state (pull_request) Failing after 37s
Repo State / repo-state (push) Failing after 27s
Validate Zacus refactor / validate (pull_request) Failing after 5m49s
feat(atelier): board field on morse/nfc/coffre/led/radio puzzles + tests + complete blocks scenario
2026-06-14 17:08:22 +02:00

848 lines
46 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Canonical Zacus Blockly catalog — single source of truth for BOTH editors:
// - Atelier webapp: import { registerZacusBlocks } from '@zacus/shared/blockly'
// - zacus-hub WebView: apps/zacus-hub/Resources/blockly/zacus_blocks.js is
// GENERATED from this file by tools/dev/sync_blockly_catalog.sh (strips
// `export` keywords and appends the legacy window.* globals + auto-register).
// Never edit the hub copy by hand.
//
// Block kinds match tools/zacus-gateway/blocks_to_runtime3.py (without the
// `zacus_` prefix used for the Blockly type names). Shapes follow Scratch
// conventions: hat (sceneStart), cap (sceneEnd), statement (most), c-block
// (logicIf with "alors"/"sinon" slots).
export const ZACUS_COLOR = {
scene: "#5085F2",
npc: "#D85B73",
audio: "#A772D9",
lcd: "#338CBF",
hardware: "#2EAD8C",
espnow: "#F28033",
box3: "#8C5A33",
m5: "#4D73A6",
plip: "#C74D8C",
logic: "#F2A633",
puzzle: "#59A86C",
operator: "#59A659",
control: "#D9A032",
gm: "#7A5CCC",
};
// Vague 1 — opérateurs de comparaison (sérialisés en string d'expression
// dans la grammaire « lâche » déjà utilisée par les conditions du scénario,
// ex `score > 5`). value = symbole émis dans la string.
export const ZACUS_COMPARE_OPS = [
["=", "=="], ["≠", "!="], ["<", "<"], ["≤", "<="], [">", ">"], ["≥", ">="],
];
export const ZACUS_BOOL_OPS = [
["et", "and"], ["ou", "or"],
];
export const ZACUS_ARITH_OPS = [
["+", "+"], ["", "-"], ["×", "*"], ["÷", "/"],
];
// Effets d'affichage de la vue scène du maître (puzzle_binding.h).
export const ZACUS_SCENE_EFFECTS = [
["pulse", "pulse"],
["glitch", "glitch"],
["gyro", "gyro"],
["aucun", "none"],
];
// « Zacus dit » voice options. `voice` maps to the voice-bridge `voice_ref`
// (reference clip for F5-TTS voice cloning); emotion + rate are stored in the
// IR (tts_say) as forward-compat fields — the bridge ignores them until it
// learns them.
export const ZACUS_VOICES = [
["Zacus", "zacus"],
["Narrateur", "narrateur"],
["Assistant", "assistant"],
];
export const ZACUS_EMOTIONS = [
["neutre", "neutral"],
["joyeux", "happy"],
["inquiet", "worried"],
["mystérieux", "mysterious"],
["pressé", "urgent"],
];
// ── Vague 2 : couche « Maître du jeu » (game-master adaptatif) ─────────
// Vocabulaire repris de game/scenarios/zacus_v3_complete.yaml (phases,
// npc_mood_rules, hint_escalation, scoring conditionnel, group_classification).
// Déclencheurs de phase (phases.*.trigger). game_start est le seul présent
// dans le scénario ; les autres couvrent l'enchaînement profiling→outro.
export const ZACUS_PHASE_TRIGGERS = [
["démarrage partie", "game_start"],
["phase précédente finie", "phase_done"],
["profilage terminé", "profiling_done"],
["manuel", "manual"],
];
// Humeurs NPC adaptatives (npc_mood_rules.*.mood).
export const ZACUS_NPC_MOODS = [
["neutre", "NEUTRAL"],
["impressionné", "IMPRESSED"],
["inquiet", "WORRIED"],
["pressé", "URGENT"],
["amusé", "AMUSED"],
];
// Actions adaptatives (npc_mood_rules.*.action + game-master loop).
export const ZACUS_GM_ACTIONS = [
["indice proactif", "proactive_hint"],
["passer au suivant", "skip_to_next"],
["ajouter énigme bonus", "add_bonus_puzzle"],
];
// Types de pénalité de score (scoring.*).
export const ZACUS_PENALTY_KINDS = [
["par minute", "time_penalty_per_minute"],
["par indice", "hint_penalty"],
];
// ── Vagues 3 & 4 : énigmes manquantes + assemblage du code final ──────
// Vocabulaire repris de game/scenarios/zacus_v3_complete.yaml :
// - P2 Circuit LED, P4 Fréquence Radio, P5 Code Morse, P6 Symboles NFC,
// P7 Coffre Final (puzzles.*),
// - phases.climax.code_assembly (mapping puzzle→position de chiffres).
// Mode du décodage morse (P5.difficulty_variants : full_morse_decode vs
// morse_light_mode où l'émission est lumineuse au lieu d'être sonore).
export const ZACUS_MORSE_MODES = [
["sonore (normal)", "normal"],
["lumineux (light)", "light"],
];
// ── Phase D2 : binding carte ──────────────────────────────────────────
// Cartes connues = clés de tools/zacus-gateway/boards.yaml. Le champ `board`
// (défaut `master`) sur les énigmes réelles et le bloc contexte `hwOnBoard`
// ciblent l'exécution sur une carte précise. Garde cette liste en miroir de
// VALID_BOARDS côté Python (tools/scenario/runtime3_common.py) et de boards.yaml.
export const ZACUS_BOARDS = [
["Master", "master"],
["BOX-3", "box3"],
["PLIP", "plip"],
["Coffre P7", "p7_coffre"],
];
export function registerZacusBlocks(Blockly) {
function def(name, init) { Blockly.Blocks[name] = { init: init }; }
function stack(t, color) { t.setPreviousStatement(true, null); t.setNextStatement(true, null); t.setColour(color); }
function dropdown(opts) {
// accept [["label","value"], …] or ["value", …]
return new Blockly.FieldDropdown(opts.map(o => Array.isArray(o) ? o : [o, o]));
}
// Blockly >=10 ships FieldMultilineInput as a plugin, not core. Fall back to
// a plain text input so the catalog works against both bundles.
const MultilineInput = Blockly.FieldMultilineInput || Blockly.FieldTextInput;
// Scènes
def("zacus_sceneStart", function () {
this.appendDummyInput().appendField("▶ Début de scène").appendField(new Blockly.FieldTextInput("intro"), "id");
this.setNextStatement(true, null); this.setColour(ZACUS_COLOR.scene); this.setInputsInline(true);
});
def("zacus_sceneEnd", function () {
this.appendDummyInput().appendField("⏹ Fin de scène");
this.setPreviousStatement(true, null); this.setColour(ZACUS_COLOR.scene);
});
def("zacus_sceneGoto", function () {
this.appendDummyInput().appendField("Aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
});
def("zacus_sceneBranch", function () {
// condition : soit un reporter branché (appendValueInput "condition"),
// soit le champ texte de repli (rétrocompat strings opaques).
this.appendValueInput("condition").setCheck(null).appendField("Branche si");
this.appendDummyInput().appendField("ou texte").appendField(new Blockly.FieldTextInput("score > 5"), "condition");
this.appendDummyInput().appendField("alors").appendField(new Blockly.FieldTextInput("scene_a"), "ifTrue");
this.appendDummyInput().appendField("sinon").appendField(new Blockly.FieldTextInput("scene_b"), "ifFalse");
stack(this, ZACUS_COLOR.scene);
});
// NPC — « Zacus dit » porte la voix (voice_ref), l'émotion et la vitesse.
def("zacus_npcSay", function () {
this.appendDummyInput().appendField("Zacus dit")
.appendField(new MultilineInput("Bonjour, je suis le professeur Zacus."), "text");
this.appendDummyInput().appendField("voix").appendField(dropdown(ZACUS_VOICES), "voice")
.appendField("émotion").appendField(dropdown(ZACUS_EMOTIONS), "emotion")
.appendField("vitesse").appendField(new Blockly.FieldNumber(1, 0.5, 2, 0.1), "rate");
stack(this, ZACUS_COLOR.npc);
});
def("zacus_npcWaitResponse", function () {
this.appendDummyInput().appendField("Attendre réponse — timeout")
.appendField(new Blockly.FieldNumber(10, 1, 120, 1), "timeout_s").appendField("s");
stack(this, ZACUS_COLOR.npc); this.setInputsInline(true);
});
def("zacus_npcIntentMatch", function () {
this.appendDummyInput().appendField("Si intent =").appendField(new Blockly.FieldTextInput("yes"), "intent");
this.appendDummyInput().appendField("alors aller à").appendField(new Blockly.FieldTextInput("scene_x"), "then");
stack(this, ZACUS_COLOR.npc);
});
// Audio
def("zacus_hwSoundPlay", function () {
this.appendDummyInput().appendField("🔊 Jouer son").appendField(new Blockly.FieldTextInput("sting_win"), "asset");
stack(this, ZACUS_COLOR.audio); this.setInputsInline(true);
});
def("zacus_hwAudioStop", function () {
this.appendDummyInput().appendField("⏸ Stopper l'audio");
stack(this, ZACUS_COLOR.audio);
});
def("zacus_hwAudioVolume", function () {
this.appendDummyInput().appendField("Volume")
.appendField(new Blockly.FieldNumber(70, 0, 100, 1), "level").appendField("/100");
stack(this, ZACUS_COLOR.audio); this.setInputsInline(true);
});
// Phase D3 — média réel SD : joue un fichier par CHEMIN SD réel (ex.
// zacus/<board>/<file>.wav), résolu par le firmware (/sdcard/… ou
// /littlefs/…). Le `board` vient du contexte hwOnBoard (défaut master),
// PAS d'un champ ici. Compile → action sound_play{path, board, loop}. Le
// legacy zacus_hwSoundPlay(asset opaque) reste inchangé.
def("zacus_mediaPlay", function () {
this.appendDummyInput().appendField("♫ Jouer média")
.appendField(new Blockly.FieldTextInput("zacus/master/intro.wav"), "path");
this.appendDummyInput().appendField("boucle").appendField(new Blockly.FieldCheckbox("FALSE"), "loop");
stack(this, ZACUS_COLOR.audio); this.setInputsInline(true);
});
// LCD
def("zacus_hwLCDText", function () {
this.appendDummyInput().appendField("LCD ligne")
.appendField(new Blockly.FieldNumber(0, 0, 32, 1), "line")
.appendField("texte").appendField(new Blockly.FieldTextInput("Bienvenue"), "text");
stack(this, ZACUS_COLOR.lcd); this.setInputsInline(true);
});
def("zacus_hwLCDClear", function () {
this.appendDummyInput().appendField("LCD — effacer");
stack(this, ZACUS_COLOR.lcd);
});
def("zacus_hwLCDImage", function () {
this.appendDummyInput().appendField("LCD image").appendField(new Blockly.FieldTextInput("splash"), "asset");
stack(this, ZACUS_COLOR.lcd); this.setInputsInline(true);
});
def("zacus_hwLCDTouchWait", function () {
this.appendDummyInput().appendField("Attendre tap zone").appendField(new Blockly.FieldTextInput("btn_yes"), "zone")
.appendField("timeout").appendField(new Blockly.FieldNumber(30, 0, 600, 1), "timeout_s").appendField("s");
stack(this, ZACUS_COLOR.lcd); this.setInputsInline(true);
});
// Hardware (ESP32 générique)
def("zacus_hwServo", function () {
this.appendDummyInput().appendField("Servo canal")
.appendField(new Blockly.FieldNumber(0, 0, 15, 1), "channel")
.appendField("→ angle").appendField(new Blockly.FieldNumber(90, 0, 180, 1), "angle").appendField("°");
stack(this, ZACUS_COLOR.hardware); this.setInputsInline(true);
});
def("zacus_hwReadQR", function () {
this.appendDummyInput().appendField("QR — attendu").appendField(new Blockly.FieldTextInput("ZAC-A1"), "expected");
stack(this, ZACUS_COLOR.hardware); this.setInputsInline(true);
});
def("zacus_hwLEDPattern", function () {
this.appendDummyInput().appendField("LED motif").appendField(dropdown([
["arc-en-ciel","rainbow"],["clignote","blink"],["fondu","fade"],["éteindre","off"]
]), "pattern");
stack(this, ZACUS_COLOR.hardware); this.setInputsInline(true);
});
def("zacus_hwBuzzerTone", function () {
this.appendDummyInput().appendField("Buzzer")
.appendField(new Blockly.FieldNumber(2000, 20, 20000, 1), "freq").appendField("Hz pendant")
.appendField(new Blockly.FieldNumber(120, 1, 10000, 1), "ms").appendField("ms");
stack(this, ZACUS_COLOR.hardware); this.setInputsInline(true);
});
def("zacus_hwRelay", function () {
this.appendDummyInput().appendField("Relais canal")
.appendField(new Blockly.FieldNumber(0, 0, 15, 1), "channel")
.appendField(dropdown([["allumer","on"],["éteindre","off"],["impulsion","pulse"]]), "state");
stack(this, ZACUS_COLOR.hardware); this.setInputsInline(true);
});
def("zacus_hwSensorRead", function () {
this.appendDummyInput().appendField("Lire capteur").appendField(new Blockly.FieldTextInput("A0"), "pin")
.appendField("→").appendField(new Blockly.FieldTextInput("lecture"), "var");
stack(this, ZACUS_COLOR.hardware); this.setInputsInline(true);
});
def("zacus_hwButtonWait", function () {
this.appendDummyInput().appendField("Attendre bouton").appendField(new Blockly.FieldTextInput("btn_main"), "button")
.appendField("timeout").appendField(new Blockly.FieldNumber(0, 0, 600, 1), "timeout_s").appendField("s");
stack(this, ZACUS_COLOR.hardware); this.setInputsInline(true);
});
// ESP-NOW
def("zacus_espnowRegisterPeer", function () {
this.appendDummyInput().appendField("ESP-NOW alias").appendField(new Blockly.FieldTextInput("annexe1"), "alias")
.appendField("MAC").appendField(new Blockly.FieldTextInput("AA:BB:CC:DD:EE:FF"), "mac");
stack(this, ZACUS_COLOR.espnow); this.setInputsInline(true);
});
def("zacus_espnowSend", function () {
this.appendDummyInput().appendField("ESP-NOW → peer").appendField(new Blockly.FieldTextInput("annexe1"), "peer")
.appendField("cmd").appendField(new Blockly.FieldTextInput("open_door"), "command");
stack(this, ZACUS_COLOR.espnow); this.setInputsInline(true);
});
def("zacus_espnowBroadcast", function () {
this.appendDummyInput().appendField("ESP-NOW broadcast").appendField(new Blockly.FieldTextInput("reset"), "command");
stack(this, ZACUS_COLOR.espnow); this.setInputsInline(true);
});
def("zacus_espnowWait", function () {
this.appendDummyInput().appendField("ESP-NOW attendre").appendField(new Blockly.FieldTextInput("ready"), "command")
.appendField("timeout").appendField(new Blockly.FieldNumber(10, 0, 600, 1), "timeout_s").appendField("s");
stack(this, ZACUS_COLOR.espnow); this.setInputsInline(true);
});
// ESP32-S3-BOX-3
def("zacus_boxIMUShake", function () {
this.appendDummyInput().appendField("BOX-3 secousse seuil")
.appendField(new Blockly.FieldNumber(1.5, 0.1, 10, 0.1), "threshold")
.appendField("g timeout").appendField(new Blockly.FieldNumber(10, 0, 600, 1), "timeout_s").appendField("s");
stack(this, ZACUS_COLOR.box3); this.setInputsInline(true);
});
def("zacus_boxIRSend", function () {
this.appendDummyInput().appendField("BOX-3 IR").appendField(dropdown(["NEC","RC5","SONY","RAW"]), "protocol")
.appendField(new Blockly.FieldTextInput("0x20DF10EF"), "code");
stack(this, ZACUS_COLOR.box3); this.setInputsInline(true);
});
// M5Stack / M5StickC
def("zacus_m5Beep", function () {
this.appendDummyInput().appendField("M5 bip")
.appendField(new Blockly.FieldNumber(4000, 20, 20000, 1), "freq").appendField("Hz")
.appendField(new Blockly.FieldNumber(200, 1, 5000, 1), "ms").appendField("ms");
stack(this, ZACUS_COLOR.m5); this.setInputsInline(true);
});
def("zacus_m5LCDText", function () {
this.appendDummyInput().appendField("M5 LCD").appendField(new Blockly.FieldTextInput("Hello"), "text")
.appendField(dropdown(["white","yellow","red","green","blue","cyan","magenta"]), "color")
.appendField("size").appendField(new Blockly.FieldNumber(2, 1, 8, 1), "size");
stack(this, ZACUS_COLOR.m5); this.setInputsInline(true);
});
def("zacus_m5ButtonAB", function () {
this.appendDummyInput().appendField("M5 bouton").appendField(dropdown(["A","B","C","any"]), "button")
.appendField("timeout").appendField(new Blockly.FieldNumber(0, 0, 600, 1), "timeout_s").appendField("s");
stack(this, ZACUS_COLOR.m5); this.setInputsInline(true);
});
def("zacus_m5RGBLed", function () {
this.appendDummyInput().appendField("M5 LED RGB").appendField(new Blockly.FieldTextInput("#FF8800"), "color");
stack(this, ZACUS_COLOR.m5); this.setInputsInline(true);
});
def("zacus_m5IMUShake", function () {
this.appendDummyInput().appendField("M5 secousse seuil")
.appendField(new Blockly.FieldNumber(1.5, 0.1, 10, 0.1), "threshold")
.appendField("g timeout").appendField(new Blockly.FieldNumber(10, 0, 600, 1), "timeout_s").appendField("s");
stack(this, ZACUS_COLOR.m5); this.setInputsInline(true);
});
// PLIP téléphone
def("zacus_plipRing", function () {
this.appendDummyInput().appendField("☎ PLIP sonner")
.appendField(new Blockly.FieldNumber(3, 0, 60, 0.1), "duration_s").appendField("s");
stack(this, ZACUS_COLOR.plip); this.setInputsInline(true);
});
def("zacus_plipPickupWait", function () {
this.appendDummyInput().appendField("☎ PLIP attendre décroché — timeout")
.appendField(new Blockly.FieldNumber(30, 0, 600, 1), "timeout_s").appendField("s");
stack(this, ZACUS_COLOR.plip); this.setInputsInline(true);
});
// Énigmes locales du maître — ces blocs ne produisent pas d'actions : ils
// attachent les objets IR `scene` / `puzzle` au step courant (un seul de
// chaque par step, voir specs/ZACUS_RUNTIME_3_SPEC.md « Step extras »).
def("zacus_scene", function () {
this.appendDummyInput().appendField("🖥 Écran maître — titre")
.appendField(new Blockly.FieldTextInput("MISSION QR"), "title");
this.appendDummyInput().appendField("sous-titre")
.appendField(new Blockly.FieldTextInput("scannez dans l'ordre"), "subtitle");
this.appendDummyInput().appendField("symbole").appendField(new Blockly.FieldTextInput("RUN"), "symbol")
.appendField("effet").appendField(dropdown(ZACUS_SCENE_EFFECTS), "effect");
stack(this, ZACUS_COLOR.puzzle);
});
def("zacus_puzzleQR", function () {
this.appendDummyInput().appendField("🧩 Énigme QR — slot")
.appendField(new Blockly.FieldNumber(3, 1, 8, 1), "id");
this.appendDummyInput().appendField("codes (dans l'ordre, virgules)")
.appendField(new Blockly.FieldTextInput("zacus-qr-1, zacus-qr-2"), "codes");
this.appendDummyInput().appendField("fragment (1-4 chiffres)")
.appendField(new Blockly.FieldTextInput("3"), "fragment");
this.appendDummyInput().appendField("carte").appendField(dropdown(ZACUS_BOARDS), "board");
stack(this, ZACUS_COLOR.puzzle);
});
def("zacus_puzzleSound", function () {
this.appendDummyInput().appendField("🧩 Énigme son — slot")
.appendField(new Blockly.FieldNumber(1, 1, 8, 1), "id");
this.appendDummyInput().appendField("mélodie (notes MIDI, virgules)")
.appendField(new Blockly.FieldTextInput("60, 62, 64, 65"), "melody")
.appendField("tolérance").appendField(new Blockly.FieldNumber(1, 0, 12, 1), "tolerance");
this.appendDummyInput().appendField("fragment (1-4 chiffres)")
.appendField(new Blockly.FieldTextInput("1"), "fragment");
this.appendDummyInput().appendField("carte").appendField(dropdown(ZACUS_BOARDS), "board");
stack(this, ZACUS_COLOR.puzzle);
});
// ── Vague 3 : énigmes manquantes (P2/P4/P5/P6/P7) ─────────────────
// Même pattern que puzzleQR/puzzleSound : step-extra `puzzle` (pas une
// action), slot id 1-8, paramètres propres à l'énigme + fragment 1-4
// chiffres. Couleur ZACUS_COLOR.puzzle.
def("zacus_puzzleLED", function () {
this.appendDummyInput().appendField("🧩 Énigme circuit LED — slot")
.appendField(new Blockly.FieldNumber(2, 1, 8, 1), "id");
this.appendDummyInput().appendField("motif attendu (composants/états, virgules)")
.appendField(new Blockly.FieldTextInput("R1, LED, SW1"), "pattern");
this.appendDummyInput().appendField("fragment (1-4 chiffres)")
.appendField(new Blockly.FieldTextInput("3"), "fragment");
this.appendDummyInput().appendField("carte").appendField(dropdown(ZACUS_BOARDS), "board");
stack(this, ZACUS_COLOR.puzzle);
});
def("zacus_puzzleRadio", function () {
this.appendDummyInput().appendField("🧩 Énigme radio — slot")
.appendField(new Blockly.FieldNumber(4, 1, 8, 1), "id");
this.appendDummyInput().appendField("fréquence cible")
.appendField(new Blockly.FieldNumber(1337, 0, 100000, 1), "frequency").appendField("Hz")
.appendField("± tolérance").appendField(new Blockly.FieldNumber(10, 0, 10000, 1), "tolerance").appendField("Hz");
this.appendDummyInput().appendField("fragment (1-4 chiffres)")
.appendField(new Blockly.FieldTextInput("4"), "fragment");
this.appendDummyInput().appendField("carte").appendField(dropdown(ZACUS_BOARDS), "board");
stack(this, ZACUS_COLOR.puzzle);
});
def("zacus_puzzleMorse", function () {
this.appendDummyInput().appendField("🧩 Énigme morse — slot")
.appendField(new Blockly.FieldNumber(5, 1, 8, 1), "id");
this.appendDummyInput().appendField("message")
.appendField(new Blockly.FieldTextInput("ZACUS"), "message")
.appendField("mode").appendField(dropdown(ZACUS_MORSE_MODES), "mode");
this.appendDummyInput().appendField("fragment (1-4 chiffres)")
.appendField(new Blockly.FieldTextInput("5"), "fragment");
this.appendDummyInput().appendField("carte").appendField(dropdown(ZACUS_BOARDS), "board");
stack(this, ZACUS_COLOR.puzzle);
});
def("zacus_puzzleNFC", function () {
this.appendDummyInput().appendField("🧩 Énigme symboles NFC — slot")
.appendField(new Blockly.FieldNumber(6, 1, 8, 1), "id");
this.appendDummyInput().appendField("tags attendus (ordre, virgules)")
.appendField(new Blockly.FieldTextInput("7, 2, 11, 4, 9, 1, 8, 3, 12, 6, 10, 5"), "tags");
this.appendDummyInput().appendField("ordre imposé").appendField(new Blockly.FieldCheckbox("TRUE"), "ordered");
this.appendDummyInput().appendField("fragment (1-4 chiffres)")
.appendField(new Blockly.FieldTextInput("67"), "fragment");
this.appendDummyInput().appendField("carte").appendField(dropdown(ZACUS_BOARDS), "board");
stack(this, ZACUS_COLOR.puzzle);
});
def("zacus_puzzleCoffre", function () {
this.appendDummyInput().appendField("🧩 Énigme coffre — slot")
.appendField(new Blockly.FieldNumber(7, 1, 8, 1), "id");
this.appendDummyInput().appendField("longueur du code")
.appendField(new Blockly.FieldNumber(8, 1, 16, 1), "length").appendField("chiffres");
this.appendDummyInput().appendField("fragment (1-4 chiffres)")
.appendField(new Blockly.FieldTextInput("8"), "fragment");
this.appendDummyInput().appendField("carte").appendField(dropdown(ZACUS_BOARDS), "board");
stack(this, ZACUS_COLOR.puzzle);
});
// Bloc matériel : lecture d'un tag NFC hors énigme (catégorie Matériel).
def("zacus_hwNFCRead", function () {
this.appendDummyInput().appendField("NFC — attendu").appendField(new Blockly.FieldTextInput("tag_07"), "expected")
.appendField("→").appendField(new Blockly.FieldTextInput("nfc_uid"), "var")
.appendField("timeout").appendField(new Blockly.FieldNumber(30, 0, 600, 1), "timeout_s").appendField("s");
stack(this, ZACUS_COLOR.hardware); this.setInputsInline(true);
});
// ── Vague 4 : assemblage du code final / coffre ───────────────────
// Actions (pas des step-extras) mappées aux kinds IR code_assembly /
// coffre_verify / code_fragment. phases.climax.code_assembly.
// Assemble le code final : longueur totale + mapping puzzle→chiffres
// (ex « P1_SON:1-2, P2_CIRCUIT:3, P4_RADIO:4 … »).
def("zacus_codeAssembly", function () {
this.appendDummyInput().appendField("🔐 Assembler le code — longueur")
.appendField(new Blockly.FieldNumber(8, 1, 16, 1), "length").appendField("chiffres");
this.appendDummyInput().appendField("fragments (puzzle:positions, virgules)")
.appendField(new Blockly.FieldTextInput("P1_SON:1-2, P2_CIRCUIT:3, P4_RADIO:4, P5_MORSE:5, P6_SYMBOLES:6-7, P3_QR:8"), "fragments");
stack(this, ZACUS_COLOR.puzzle);
});
// Associe explicitement un fragment de puzzle à une (ou des) position(s).
def("zacus_codeFragment", function () {
this.appendDummyInput().appendField("↳ Fragment puzzle")
.appendField(new Blockly.FieldTextInput("P1_SON"), "puzzle")
.appendField("→ positions").appendField(new Blockly.FieldTextInput("1-2"), "positions");
stack(this, ZACUS_COLOR.puzzle); this.setInputsInline(true);
});
// Vérifie le code assemblé : déclenche l'ouverture / la fin.
def("zacus_coffreVerify", function () {
this.appendDummyInput().appendField("✅ Vérifier coffre — slot")
.appendField(new Blockly.FieldNumber(7, 1, 8, 1), "id");
this.appendDummyInput().appendField("au succès aller à")
.appendField(new Blockly.FieldTextInput("outro"), "on_success");
stack(this, ZACUS_COLOR.puzzle);
});
// ── Phase D2 : binding carte ──────────────────────────────────────
// Bloc contexte c-block « ▸ sur la carte X { … } » : il ne produit aucune
// action propre, il TAGUE chaque action de son slot `body` avec le `board`
// choisi (cf. blocks_to_runtime3.py). previous+next connectés. Réel : le
// routage gateway s'appuie dessus.
def("zacus_hwOnBoard", function () {
this.appendDummyInput().appendField("▸ sur la carte").appendField(dropdown(ZACUS_BOARDS), "board");
this.appendStatementInput("body").setCheck(null).appendField("faire");
stack(this, ZACUS_COLOR.hardware);
});
// ── Phase D4 : transitions réelles ────────────────────────────────
// Blocs « Quand … → aller à [target] » : chacun émet UNE transition avec le
// vrai event_type firmware-backed (button/serial/audio_done/unlock/espnow).
// event_type ∈ runtime3_common.EVENT_TYPES (pas d'extension de schéma, juste
// de l'émission). target = id d'étape cible (même pattern que sceneGoto).
// Voie RÉELLE : consommés côté gateway/web (le firmware n'a pas de moteur de
// transitions, il est piloté pas-à-pas). Compile dans blocks_to_runtime3.py.
def("zacus_onButton", function () {
this.appendDummyInput().appendField("Quand bouton")
.appendField(new Blockly.FieldTextInput("NEXT"), "button")
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
});
def("zacus_onAudioDone", function () {
this.appendDummyInput().appendField("Quand audio terminé → aller à")
.appendField(new Blockly.FieldTextInput("next_scene"), "target");
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
});
def("zacus_onUnlock", function () {
this.appendDummyInput().appendField("Quand déverrouillage")
.appendField(new Blockly.FieldTextInput("door"), "source")
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
});
def("zacus_onSerial", function () {
this.appendDummyInput().appendField("Quand série reçoit")
.appendField(new Blockly.FieldTextInput("OK"), "token")
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
});
def("zacus_onEspnow", function () {
this.appendDummyInput().appendField("Quand ESP-NOW reçoit")
.appendField(new Blockly.FieldTextInput("ready"), "command")
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
});
def("zacus_onPuzzleSolved", function () {
this.appendDummyInput().appendField("Quand énigme résolue")
.appendField(new Blockly.FieldTextInput("1"), "puzzle")
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
});
def("zacus_onCodeAssembled", function () {
this.appendDummyInput().appendField("Quand code assemblé")
.appendField(new Blockly.FieldTextInput("4242"), "code")
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
});
// Logique
def("zacus_logicIf", function () {
// condition : reporter branché (appendValueInput "condition") OU champ
// texte de repli (rétrocompat strings opaques).
this.appendValueInput("condition").setCheck(null).appendField("si");
this.appendDummyInput().appendField("ou texte").appendField(new Blockly.FieldTextInput("score > 0"), "condition");
this.appendStatementInput("body").setCheck(null).appendField("alors");
this.appendStatementInput("else").setCheck(null).appendField("sinon");
stack(this, ZACUS_COLOR.logic);
});
def("zacus_logicTimer", function () {
this.appendDummyInput().appendField("Attendre")
.appendField(new Blockly.FieldNumber(5, 0, 600, 0.1), "seconds").appendField("s");
stack(this, ZACUS_COLOR.logic); this.setInputsInline(true);
});
def("zacus_logicScore", function () {
this.appendDummyInput().appendField("Score +")
.appendField(new Blockly.FieldNumber(1, -100, 100, 1), "delta");
stack(this, ZACUS_COLOR.logic); this.setInputsInline(true);
});
def("zacus_logicSetVar", function () {
this.appendDummyInput().appendField("Variable").appendField(new Blockly.FieldTextInput("key"), "name")
.appendField(":=").appendField(new Blockly.FieldTextInput("42"), "value");
stack(this, ZACUS_COLOR.logic); this.setInputsInline(true);
});
// ── Vague 1 : Opérateurs (reporters) ──────────────────────────────
// Tous setOutput(true) : pas de previous/next statement, ils se branchent
// dans un appendValueInput (condition de logicIf/loopWhile/sceneBranch ou
// un opérande d'un autre opérateur). Sérialisés en string d'expression.
def("zacus_opCompare", function () {
this.appendValueInput("A").setCheck(null);
this.appendDummyInput().appendField(dropdown(ZACUS_COMPARE_OPS), "op");
this.appendValueInput("B").setCheck(null);
this.setInputsInline(true); this.setOutput(true, null); this.setColour(ZACUS_COLOR.operator);
});
def("zacus_opBool", function () {
this.appendValueInput("A").setCheck(null);
this.appendDummyInput().appendField(dropdown(ZACUS_BOOL_OPS), "op");
this.appendValueInput("B").setCheck(null);
this.setInputsInline(true); this.setOutput(true, null); this.setColour(ZACUS_COLOR.operator);
});
def("zacus_opNot", function () {
this.appendValueInput("A").setCheck(null).appendField("non");
this.setInputsInline(true); this.setOutput(true, null); this.setColour(ZACUS_COLOR.operator);
});
def("zacus_opArith", function () {
this.appendValueInput("A").setCheck(null);
this.appendDummyInput().appendField(dropdown(ZACUS_ARITH_OPS), "op");
this.appendValueInput("B").setCheck(null);
this.setInputsInline(true); this.setOutput(true, null); this.setColour(ZACUS_COLOR.operator);
});
def("zacus_valVar", function () {
this.appendDummyInput().appendField("variable").appendField(new Blockly.FieldTextInput("pct"), "name");
this.setOutput(true, null); this.setColour(ZACUS_COLOR.operator); this.setInputsInline(true);
});
def("zacus_valScore", function () {
this.appendDummyInput().appendField("score courant");
this.setOutput(true, null); this.setColour(ZACUS_COLOR.operator); this.setInputsInline(true);
});
def("zacus_valNumber", function () {
this.appendDummyInput().appendField(new Blockly.FieldNumber(0), "num");
this.setOutput(true, null); this.setColour(ZACUS_COLOR.operator); this.setInputsInline(true);
});
def("zacus_valText", function () {
this.appendDummyInput().appendField("“").appendField(new Blockly.FieldTextInput("texte"), "text").appendField("”");
this.setOutput(true, null); this.setColour(ZACUS_COLOR.operator); this.setInputsInline(true);
});
// ── Vague 1 : Contrôle (statements c-block) ───────────────────────
def("zacus_loopRepeat", function () {
this.appendDummyInput().appendField("répéter")
.appendField(new Blockly.FieldNumber(10, 0, 100000, 1), "times").appendField("fois");
this.appendStatementInput("body").setCheck(null).appendField("faire");
stack(this, ZACUS_COLOR.control);
});
def("zacus_loopWhile", function () {
this.appendValueInput("condition").setCheck(null).appendField("tant que");
this.appendDummyInput().appendField("ou texte").appendField(new Blockly.FieldTextInput("score < 5"), "condition_text");
this.appendStatementInput("body").setCheck(null).appendField("faire");
stack(this, ZACUS_COLOR.control);
});
// ── Vague 2 : Maître du jeu (game-master adaptatif) ───────────────
// Définit une phase de jeu (intro/profiling/adaptive/climax/outro) avec
// une durée et un déclencheur. phases.*.{id,duration_minutes,trigger}.
def("zacus_phase", function () {
this.appendDummyInput().appendField("🎬 Phase").appendField(new Blockly.FieldTextInput("adaptive"), "id");
this.appendDummyInput().appendField("durée")
.appendField(new Blockly.FieldNumber(20, 0, 240, 1), "duration_minutes").appendField("min");
this.appendDummyInput().appendField("déclencheur").appendField(dropdown(ZACUS_PHASE_TRIGGERS), "trigger");
stack(this, ZACUS_COLOR.gm);
});
// Règle condition→action du game-master (npc_mood_rules.*). La condition
// accepte un reporter branché OU un texte (comme logicIf).
def("zacus_gmRule", function () {
this.appendValueInput("condition").setCheck(null).appendField("⚖ Règle si");
this.appendDummyInput().appendField("ou texte").appendField(new Blockly.FieldTextInput("pct > 130"), "condition");
this.appendDummyInput().appendField("→ action").appendField(dropdown(ZACUS_GM_ACTIONS), "action");
stack(this, ZACUS_COLOR.gm);
});
// Humeur du NPC (npc_mood_rules.*.mood).
def("zacus_npcSetMood", function () {
this.appendDummyInput().appendField("😐 Humeur NPC").appendField(dropdown(ZACUS_NPC_MOODS), "mood");
stack(this, ZACUS_COLOR.gm); this.setInputsInline(true);
});
// Donner un indice de niveau N avec sa clé (hint_keys.level_N + give level).
def("zacus_giveHint", function () {
this.appendDummyInput().appendField("💡 Indice niveau").appendField(dropdown([["1","1"],["2","2"],["3","3"]]), "level")
.appendField("clé").appendField(new Blockly.FieldTextInput("hints.P5_MORSE.level_1"), "hint_key");
stack(this, ZACUS_COLOR.gm); this.setInputsInline(true);
});
// Indice proactif à intervalle régulier (hint_escalation.proactive_hint_interval_seconds).
def("zacus_proactiveHint", function () {
this.appendDummyInput().appendField("🔔 Indice proactif toutes les")
.appendField(new Blockly.FieldNumber(90, 1, 3600, 1), "interval_seconds").appendField("s")
.appendField("clé").appendField(new Blockly.FieldTextInput("hints.P5_MORSE.level_1"), "hint_key");
stack(this, ZACUS_COLOR.gm); this.setInputsInline(true);
});
// Définit le parcours (liste de puzzles) pour un profil/classe
// (group_classification.*.parcours).
def("zacus_setParcours", function () {
this.appendDummyInput().appendField("🧭 Parcours profil").appendField(new Blockly.FieldTextInput("TECH"), "profile");
this.appendDummyInput().appendField("puzzles (dans l'ordre, virgules)")
.appendField(new Blockly.FieldTextInput("P2_CIRCUIT, P4_RADIO, P5_MORSE"), "puzzles");
stack(this, ZACUS_COLOR.gm);
});
// Bonus conditionnel (scoring.bonus_*). condition = reporter branché OU texte.
def("zacus_scoreBonus", function () {
this.appendValueInput("condition").setCheck(null).appendField("🏆 Bonus si");
this.appendDummyInput().appendField("ou texte").appendField(new Blockly.FieldTextInput("hints_used == 0"), "condition");
this.appendDummyInput().appendField("→ +").appendField(new Blockly.FieldNumber(200, 0, 10000, 1), "points").appendField("pts");
stack(this, ZACUS_COLOR.gm);
});
// Pénalité de score (scoring.time_penalty_per_minute / hint_penalty).
def("zacus_scorePenalty", function () {
this.appendDummyInput().appendField("⛔ Pénalité").appendField(dropdown(ZACUS_PENALTY_KINDS), "penalty_kind")
.appendField("").appendField(new Blockly.FieldNumber(10, 0, 10000, 1), "points").appendField("pts");
stack(this, ZACUS_COLOR.gm); this.setInputsInline(true);
});
}
export const ZACUS_BLOCK_TYPES = [
"zacus_sceneStart","zacus_sceneEnd","zacus_sceneGoto","zacus_sceneBranch",
"zacus_onButton","zacus_onAudioDone","zacus_onUnlock","zacus_onSerial","zacus_onEspnow","zacus_onPuzzleSolved","zacus_onCodeAssembled",
"zacus_npcSay","zacus_npcWaitResponse","zacus_npcIntentMatch",
"zacus_hwSoundPlay","zacus_hwAudioStop","zacus_hwAudioVolume","zacus_mediaPlay",
"zacus_hwLCDText","zacus_hwLCDClear","zacus_hwLCDImage","zacus_hwLCDTouchWait",
"zacus_hwServo","zacus_hwReadQR","zacus_hwLEDPattern","zacus_hwBuzzerTone","zacus_hwRelay","zacus_hwSensorRead","zacus_hwButtonWait","zacus_hwNFCRead",
"zacus_espnowRegisterPeer","zacus_espnowSend","zacus_espnowBroadcast","zacus_espnowWait",
"zacus_boxIMUShake","zacus_boxIRSend",
"zacus_m5Beep","zacus_m5LCDText","zacus_m5ButtonAB","zacus_m5RGBLed","zacus_m5IMUShake",
"zacus_plipRing","zacus_plipPickupWait",
"zacus_scene","zacus_puzzleQR","zacus_puzzleSound",
"zacus_puzzleLED","zacus_puzzleRadio","zacus_puzzleMorse","zacus_puzzleNFC","zacus_puzzleCoffre",
"zacus_codeAssembly","zacus_codeFragment","zacus_coffreVerify",
"zacus_hwOnBoard",
"zacus_logicIf","zacus_logicTimer","zacus_logicScore","zacus_logicSetVar",
"zacus_opCompare","zacus_opBool","zacus_opNot","zacus_opArith",
"zacus_valVar","zacus_valScore","zacus_valNumber","zacus_valText",
"zacus_loopRepeat","zacus_loopWhile",
"zacus_phase","zacus_gmRule","zacus_npcSetMood","zacus_giveHint","zacus_proactiveHint","zacus_setParcours","zacus_scoreBonus","zacus_scorePenalty",
];
export const ZACUS_FIELDS_BY_KIND = {
zacus_sceneStart: ["id"],
zacus_sceneEnd: [],
zacus_sceneGoto: ["target"],
zacus_sceneBranch: ["condition","ifTrue","ifFalse"],
zacus_npcSay: ["text","voice","emotion","rate"],
zacus_npcWaitResponse: ["timeout_s"],
zacus_npcIntentMatch: ["intent","then"],
// Phase D4 — transitions réelles : « Quand … → aller à [target] ».
zacus_onButton: ["button","target"],
zacus_onAudioDone: ["target"],
zacus_onUnlock: ["source","target"],
zacus_onSerial: ["token","target"],
zacus_onEspnow: ["command","target"],
zacus_onPuzzleSolved: ["puzzle","target"],
zacus_onCodeAssembled: ["code","target"],
zacus_hwSoundPlay: ["asset"],
zacus_hwAudioStop: [],
zacus_hwAudioVolume: ["level"],
// Phase D3 — média réel SD : chemin SD + boucle. board hérité de hwOnBoard.
zacus_mediaPlay: ["path","loop"],
zacus_hwLCDText: ["line","text"],
zacus_hwLCDClear: [],
zacus_hwLCDImage: ["asset"],
zacus_hwLCDTouchWait: ["zone","timeout_s"],
zacus_hwServo: ["channel","angle"],
zacus_hwReadQR: ["expected"],
zacus_hwLEDPattern: ["pattern"],
zacus_hwBuzzerTone: ["freq","ms"],
zacus_hwRelay: ["channel","state"],
zacus_hwSensorRead: ["pin","var"],
zacus_hwButtonWait: ["button","timeout_s"],
zacus_hwNFCRead: ["expected","var","timeout_s"],
zacus_espnowRegisterPeer: ["alias","mac"],
zacus_espnowSend: ["peer","command"],
zacus_espnowBroadcast: ["command"],
zacus_espnowWait: ["command","timeout_s"],
zacus_boxIMUShake: ["threshold","timeout_s"],
zacus_boxIRSend: ["protocol","code"],
zacus_m5Beep: ["freq","ms"],
zacus_m5LCDText: ["text","color","size"],
zacus_m5ButtonAB: ["button","timeout_s"],
zacus_m5RGBLed: ["color"],
zacus_m5IMUShake: ["threshold","timeout_s"],
zacus_plipRing: ["duration_s"],
zacus_plipPickupWait: ["timeout_s"],
zacus_scene: ["title","subtitle","symbol","effect"],
zacus_puzzleQR: ["id","codes","fragment","board"],
zacus_puzzleSound: ["id","melody","tolerance","fragment","board"],
// Phase D2 — binding carte : bloc contexte (slot `body`, champ `board`).
zacus_hwOnBoard: ["board"],
// Vague 3 — énigmes manquantes (step-extra `puzzle`).
zacus_puzzleLED: ["id","pattern","fragment","board"],
zacus_puzzleRadio: ["id","frequency","tolerance","fragment","board"],
zacus_puzzleMorse: ["id","message","mode","fragment","board"],
zacus_puzzleNFC: ["id","tags","ordered","fragment","board"],
zacus_puzzleCoffre: ["id","length","fragment","board"],
// Vague 4 — assemblage du code final (actions).
zacus_codeAssembly: ["length","fragments"],
zacus_codeFragment: ["puzzle","positions"],
zacus_coffreVerify: ["id","on_success"],
zacus_logicIf: ["condition"],
zacus_logicTimer: ["seconds"],
zacus_logicScore: ["delta"],
zacus_logicSetVar: ["name","value"],
// Vague 1 — opérateurs (reporters). Les opérandes viennent des value
// inputs (sérialisés via `inputs`), seuls le dropdown/littéral sont des
// champs.
zacus_opCompare: ["op"],
zacus_opBool: ["op"],
zacus_opNot: [],
zacus_opArith: ["op"],
zacus_valVar: ["name"],
zacus_valScore: [],
zacus_valNumber: ["num"],
zacus_valText: ["text"],
// Vague 1 — contrôle.
zacus_loopRepeat: ["times"],
zacus_loopWhile: ["condition_text"],
// Vague 2 — Maître du jeu. Les conditions des règles/bonus sont des value
// inputs (sérialisées via `inputs`) avec repli texte `condition`.
zacus_phase: ["id","duration_minutes","trigger"],
zacus_gmRule: ["condition","action"],
zacus_npcSetMood: ["mood"],
zacus_giveHint: ["level","hint_key"],
zacus_proactiveHint: ["interval_seconds","hint_key"],
zacus_setParcours: ["profile","puzzles"],
zacus_scoreBonus: ["condition","points"],
zacus_scorePenalty: ["penalty_kind","points"],
};
// ── DSL honnête : voies « réel » vs « hardware futur » (phase D1) ───────
// Un bloc est RÉEL s'il est effectivement exécuté — soit on-device (firmware
// Freenove master : scene/QR/sound, média, relais ESP-NOW), soit côté gateway
// (logique de transitions/score/variables, couche game-master, opérateurs).
// Un bloc HARDWARE FUTUR est validé et compilé en IR comme avant, mais AUCUN
// executor ne le joue (servo, relais, buzzer, LCD tactile, NFC, énigmes
// LED/Radio/Morse/NFC/Coffre, assemblage code/coffre, cartes M5/BOX-3/PLIP).
// La compilation (blocks_to_runtime3.py) émet un *warning* non bloquant pour
// ces blocs. Garde cette liste en miroir de FUTURE_HW_KINDS côté Python (kinds
// sans le préfixe `zacus_`).
export const ZACUS_REAL_KINDS = new Set([
"zacus_sceneStart","zacus_sceneEnd","zacus_sceneGoto","zacus_sceneBranch",
"zacus_scene","zacus_puzzleQR","zacus_puzzleSound",
"zacus_hwOnBoard",
"zacus_npcSay","zacus_npcWaitResponse","zacus_npcIntentMatch",
"zacus_onButton","zacus_onAudioDone","zacus_onUnlock","zacus_onSerial","zacus_onEspnow","zacus_onPuzzleSolved","zacus_onCodeAssembled",
"zacus_hwSoundPlay","zacus_hwAudioStop","zacus_hwAudioVolume","zacus_mediaPlay",
"zacus_espnowRegisterPeer","zacus_espnowSend","zacus_espnowBroadcast","zacus_espnowWait",
"zacus_logicIf","zacus_logicTimer","zacus_logicScore","zacus_logicSetVar",
"zacus_opCompare","zacus_opBool","zacus_opNot","zacus_opArith",
"zacus_valVar","zacus_valScore","zacus_valNumber","zacus_valText",
"zacus_loopRepeat","zacus_loopWhile",
"zacus_phase","zacus_gmRule","zacus_npcSetMood","zacus_giveHint","zacus_proactiveHint",
"zacus_setParcours","zacus_scoreBonus","zacus_scorePenalty",
]);
export const ZACUS_FUTURE_HW_KINDS = new Set([
"zacus_hwLCDText","zacus_hwLCDClear","zacus_hwLCDImage","zacus_hwLCDTouchWait",
"zacus_hwServo","zacus_hwReadQR","zacus_hwLEDPattern","zacus_hwBuzzerTone",
"zacus_hwRelay","zacus_hwSensorRead","zacus_hwButtonWait","zacus_hwNFCRead",
"zacus_puzzleLED","zacus_puzzleRadio","zacus_puzzleMorse","zacus_puzzleNFC","zacus_puzzleCoffre",
"zacus_codeAssembly","zacus_codeFragment","zacus_coffreVerify",
"zacus_boxIMUShake","zacus_boxIRSend",
"zacus_m5Beep","zacus_m5LCDText","zacus_m5ButtonAB","zacus_m5RGBLed","zacus_m5IMUShake",
"zacus_plipRing","zacus_plipPickupWait",
]);
// Toolbox réorganisée en deux voies : catégories RÉELLES en haut, catégories
// « 🔬 … (futur) » regroupées en bas. Chaque type apparaît exactement une fois ;
// l'union des deux voies == ZACUS_BLOCK_TYPES.
export const ZACUS_TOOLBOX_CATEGORIES = [
// ── Voie réelle (exécutée on-device ou côté gateway) ──────────────────
{ name: "Cartes", colour: ZACUS_COLOR.hardware, types: ["zacus_hwOnBoard"] },
{ name: "Scènes", colour: ZACUS_COLOR.scene, types: ["zacus_sceneStart","zacus_sceneEnd","zacus_sceneGoto","zacus_sceneBranch"] },
{ name: "Transitions", colour: ZACUS_COLOR.scene, types: ["zacus_onButton","zacus_onAudioDone","zacus_onUnlock","zacus_onSerial","zacus_onEspnow","zacus_onPuzzleSolved","zacus_onCodeAssembled"] },
{ name: "NPC voix", colour: ZACUS_COLOR.npc, types: ["zacus_npcSay","zacus_npcWaitResponse","zacus_npcIntentMatch"] },
{ name: "Audio", colour: ZACUS_COLOR.audio, types: ["zacus_mediaPlay","zacus_hwSoundPlay","zacus_hwAudioStop","zacus_hwAudioVolume"] },
{ name: "ESP-NOW", colour: ZACUS_COLOR.espnow, types: ["zacus_espnowRegisterPeer","zacus_espnowSend","zacus_espnowBroadcast","zacus_espnowWait"] },
{ name: "Énigmes", colour: ZACUS_COLOR.puzzle, types: ["zacus_scene","zacus_puzzleQR","zacus_puzzleSound"] },
{ name: "Logique", colour: ZACUS_COLOR.logic, types: ["zacus_logicIf","zacus_logicTimer","zacus_logicScore","zacus_logicSetVar"] },
{ name: "Opérateurs", colour: ZACUS_COLOR.operator, types: ["zacus_opCompare","zacus_opBool","zacus_opNot","zacus_opArith","zacus_valVar","zacus_valScore","zacus_valNumber","zacus_valText"] },
{ name: "Contrôle", colour: ZACUS_COLOR.control, types: ["zacus_loopRepeat","zacus_loopWhile"] },
{ name: "Maître du jeu", colour: ZACUS_COLOR.gm, types: ["zacus_phase","zacus_gmRule","zacus_npcSetMood","zacus_giveHint","zacus_proactiveHint","zacus_setParcours","zacus_scoreBonus","zacus_scorePenalty"] },
// ── Voie hardware futur (validée, non exécutée on-device) ─────────────
{ name: "🔬 Énigmes (futur)", colour: ZACUS_COLOR.puzzle, types: ["zacus_puzzleLED","zacus_puzzleRadio","zacus_puzzleMorse","zacus_puzzleNFC","zacus_puzzleCoffre"] },
{ name: "🔬 Coffre (futur)", colour: ZACUS_COLOR.puzzle, types: ["zacus_codeAssembly","zacus_codeFragment","zacus_coffreVerify"] },
{ name: "🔬 LCD (futur)", colour: ZACUS_COLOR.lcd, types: ["zacus_hwLCDText","zacus_hwLCDClear","zacus_hwLCDImage","zacus_hwLCDTouchWait"] },
{ name: "🔬 Matériel (futur)", colour: ZACUS_COLOR.hardware, types: ["zacus_hwServo","zacus_hwReadQR","zacus_hwLEDPattern","zacus_hwBuzzerTone","zacus_hwRelay","zacus_hwSensorRead","zacus_hwButtonWait","zacus_hwNFCRead"] },
{ name: "🔬 BOX-3 (futur)", colour: ZACUS_COLOR.box3, types: ["zacus_boxIMUShake","zacus_boxIRSend"] },
{ name: "🔬 M5 (futur)", colour: ZACUS_COLOR.m5, types: ["zacus_m5Beep","zacus_m5LCDText","zacus_m5ButtonAB","zacus_m5RGBLed","zacus_m5IMUShake"] },
{ name: "🔬 PLIP (futur)", colour: ZACUS_COLOR.plip, types: ["zacus_plipRing","zacus_plipPickupWait"] },
];