From 0797a799a54124d4ee2154cbc5e6304be46758e0 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Fri, 6 Mar 2026 16:18:13 +0000 Subject: [PATCH 1/5] Add sensing block to configure action key bindings --- api/events.js | 15 +---- api/sensing.js | 28 +++++++-- blocks/sensing.js | 122 +++++++++++++++++++++++++++++++++++++++ flock.js | 2 + generators/generators.js | 6 ++ locale/en.js | 3 + toolbox.js | 5 ++ 7 files changed, 164 insertions(+), 17 deletions(-) diff --git a/api/events.js b/api/events.js index 364fbdb8..3b10b550 100644 --- a/api/events.js +++ b/api/events.js @@ -43,20 +43,9 @@ export const flockEvents = { } }, whenActionEvent(action, callback, isReleased = false) { - const actionMap = { - FORWARD: ["w", "z"], - BACKWARD: ["s"], - LEFT: ["a", "q"], - RIGHT: ["d"], - BUTTON1: ["e", "1"], - BUTTON2: ["r", "2"], - BUTTON3: ["f", "3"], - BUTTON4: [" ", "4"], - }; - - const actionKeys = actionMap[action]; + const actionKeys = flock.getActionKeys?.(action) ?? []; - if (!actionKeys?.length) { + if (!actionKeys.length) { return; } diff --git a/api/sensing.js b/api/sensing.js index 8e613d04..0248cc3e 100644 --- a/api/sensing.js +++ b/api/sensing.js @@ -326,8 +326,8 @@ export const flockSensing = { ); } }, - actionPressed(action) { - const actionMap = { + getActionKeys(action) { + const defaultActionMap = { FORWARD: ["W", "Z"], BACKWARD: ["S"], LEFT: ["A", "Q"], @@ -338,9 +338,29 @@ export const flockSensing = { BUTTON4: ["SPACE", " ", "4"], }; - const actionKeys = actionMap[action]; + const customKeys = flock.actionKeyMap?.[action] ?? []; + const defaultKeys = defaultActionMap[action] ?? []; + + return [...new Set([...customKeys, ...defaultKeys])]; + }, + setActionKey(action, key) { + if (!action || typeof key !== "string") { + return; + } + + if (!flock.actionKeyMap) { + flock.actionKeyMap = {}; + } + + const normalizedKey = key === " " ? "SPACE" : key.toUpperCase(); + const existingKeys = flock.actionKeyMap[action] ?? []; + + flock.actionKeyMap[action] = [...new Set([...existingKeys, normalizedKey])]; + }, + actionPressed(action) { + const actionKeys = this.getActionKeys(action); - if (!actionKeys) { + if (!actionKeys.length) { return false; } diff --git a/blocks/sensing.js b/blocks/sensing.js index 8e9a89dd..a6c2bae6 100644 --- a/blocks/sensing.js +++ b/blocks/sensing.js @@ -143,6 +143,128 @@ export function defineSensingBlocks() { }, }; + Blockly.Blocks["action_control"] = { + init: function () { + this.jsonInit({ + type: "action_control", + message0: translate("action_control"), + args0: [ + { + type: "field_dropdown", + name: "ACTION", + options: [ + [ + getOption( + "ACTION_FORWARD", + ), + "FORWARD", + ], + [ + getOption( + "ACTION_BACKWARD", + ), + "BACKWARD", + ], + [ + getOption( + "ACTION_LEFT", + ), + "LEFT", + ], + [ + getOption( + "ACTION_RIGHT", + ), + "RIGHT", + ], + [ + getOption( + "ACTION_BUTTON1", + ), + "BUTTON1", + ], + [ + getOption( + "ACTION_BUTTON2", + ), + "BUTTON2", + ], + [ + getOption( + "ACTION_BUTTON3", + ), + "BUTTON3", + ], + [ + getOption( + "ACTION_BUTTON4", + ), + "BUTTON4", + ], + ], + }, + { + type: "field_grid_dropdown", + name: "KEY", + columns: 10, + options: [ + getDropdownOption("0"), + getDropdownOption("1"), + getDropdownOption("2"), + getDropdownOption("3"), + getDropdownOption("4"), + getDropdownOption("5"), + getDropdownOption("6"), + getDropdownOption("7"), + getDropdownOption("8"), + getDropdownOption("9"), + getDropdownOption("a"), + getDropdownOption("b"), + getDropdownOption("c"), + getDropdownOption("d"), + getDropdownOption("e"), + getDropdownOption("f"), + getDropdownOption("g"), + getDropdownOption("h"), + getDropdownOption("i"), + getDropdownOption("j"), + getDropdownOption("k"), + getDropdownOption("l"), + getDropdownOption("m"), + getDropdownOption("n"), + getDropdownOption("o"), + getDropdownOption("p"), + getDropdownOption("q"), + getDropdownOption("r"), + getDropdownOption("s"), + getDropdownOption("t"), + getDropdownOption("u"), + getDropdownOption("v"), + getDropdownOption("w"), + getDropdownOption("x"), + getDropdownOption("y"), + getDropdownOption("z"), + getDropdownOption(" "), + getDropdownOption(","), + getDropdownOption("."), + getDropdownOption("/"), + getDropdownOption("ArrowLeft"), + getDropdownOption("ArrowUp"), + getDropdownOption("ArrowRight"), + getDropdownOption("ArrowDown"), + ], + }, + ], + previousStatement: null, + nextStatement: null, + colour: categoryColours["Sensing"], + tooltip: getTooltip("action_control"), + }); + this.setHelpUrl(getHelpUrlFor(this.type)); + this.setStyle("sensing_blocks"); + }, + }; + Blockly.Blocks["meshes_touching"] = { init: function () { this.jsonInit({ diff --git a/flock.js b/flock.js index d87d278a..d1629a9e 100644 --- a/flock.js +++ b/flock.js @@ -132,6 +132,7 @@ export const flock = { canvas: { pressedKeys: null, }, + actionKeyMap: {}, abortController: null, _renderLoop: null, document: document, @@ -1240,6 +1241,7 @@ export const flock = { flock.gridKeyReleaseObservable = gridKeyReleaseObservable; flock.canvas.pressedButtons = new Set(); flock.canvas.pressedKeys = new Set(); + flock.actionKeyMap = {}; const displayScale = (window.devicePixelRatio || 1) * 0.75; // Get the device pixel ratio, default to 1 if not available flock.displayScale = displayScale; flock.BABYLON.Database.IDBStorageEnabled = true; diff --git a/generators/generators.js b/generators/generators.js index ae2fc0d1..b58d3e0e 100644 --- a/generators/generators.js +++ b/generators/generators.js @@ -3081,6 +3081,12 @@ export function defineGenerators() { ]; }; + javascriptGenerator.forBlock["action_control"] = function (block) { + const action = block.getFieldValue("ACTION"); + const key = block.getFieldValue("KEY"); + return `setActionKey("${action}", ${JSON.stringify(key)});\n`; + }; + javascriptGenerator.forBlock["key_pressed"] = function (block) { const key = block.getFieldValue("KEY"); return [`keyPressed("${key}")`, javascriptGenerator.ORDER_NONE]; diff --git a/locale/en.js b/locale/en.js index 92708b9b..973ca129 100644 --- a/locale/en.js +++ b/locale/en.js @@ -249,6 +249,7 @@ export default { // Custom block translations - Sensing blocks key_pressed: "key pressed is %1", action_pressed: "%1", + action_control: "set %1 key %2", meshes_touching: "%1 touching %2", time: "time in %1", seconds: "seconds", @@ -522,6 +523,8 @@ export default { "Return true if the specified key is pressed.\nKeyword:ispressed", action_pressed_tooltip: "Return true if the specified movement or action control is active across keyboard, touch, or XR inputs.", + action_control_tooltip: + "Set an extra keyboard key for a movement or action input.", meshes_touching_tooltip: "Return true if the two selected meshes are touching.\nKeyword: istouching", time_tooltip: "Return the current time in seconds.", diff --git a/toolbox.js b/toolbox.js index 67c0e88f..5ee756cf 100644 --- a/toolbox.js +++ b/toolbox.js @@ -2272,6 +2272,11 @@ const toolboxSensing = { type: "action_pressed", keyword: "action", }, + { + kind: "block", + type: "action_control", + keyword: "action", + }, { kind: "block", type: "mesh_exists", From 464603b4f01285d4fa529345aaa2464964040db5 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Fri, 6 Mar 2026 16:29:28 +0000 Subject: [PATCH 2/5] Fix action key binding runtime for special keys --- api/events.js | 9 ++++++++- api/sensing.js | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/api/events.js b/api/events.js index 3b10b550..40a75ca9 100644 --- a/api/events.js +++ b/api/events.js @@ -49,7 +49,14 @@ export const flockEvents = { return; } - [...new Set(actionKeys.map((key) => key.toLowerCase()))].forEach((key) => { + const eventKeys = actionKeys.map((key) => { + if (key === "SPACE" || key === " ") { + return " "; + } + return key.toLowerCase(); + }); + + [...new Set(eventKeys)].forEach((key) => { this.whenKeyEvent(key, callback, isReleased); }); diff --git a/api/sensing.js b/api/sensing.js index 0248cc3e..dadedc4c 100644 --- a/api/sensing.js +++ b/api/sensing.js @@ -352,7 +352,9 @@ export const flockSensing = { flock.actionKeyMap = {}; } - const normalizedKey = key === " " ? "SPACE" : key.toUpperCase(); + // Keep special keys (e.g. ArrowLeft, " ") as-is so runtime checks match + // browser keyboard event values. + const normalizedKey = /^[a-z]$/i.test(key) ? key.toUpperCase() : key; const existingKeys = flock.actionKeyMap[action] ?? []; flock.actionKeyMap[action] = [...new Set([...existingKeys, normalizedKey])]; From 45160fc17febca4369dcf311710725035c1720f6 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Fri, 6 Mar 2026 16:35:11 +0000 Subject: [PATCH 3/5] Capture gameplay keys globally for action bindings --- flock.js | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/flock.js b/flock.js index d1629a9e..569dce3c 100644 --- a/flock.js +++ b/flock.js @@ -1288,16 +1288,44 @@ export const flock = { { passive: false }, ); - flock.canvas.addEventListener("keydown", function (event) { + const shouldIgnoreKeyboardEvent = (event) => { + const target = event.target; + if (!target) { + return false; + } + + const tagName = target.tagName?.toLowerCase(); + return ( + target.isContentEditable || + tagName === "input" || + tagName === "textarea" || + tagName === "select" + ); + }; + + const handleKeyDown = (event) => { + if (shouldIgnoreKeyboardEvent(event)) { + return; + } + flock.canvas.currentKeyPressed = event.key; flock.canvas.pressedKeys.add(event.key); - }); + }; + + const handleKeyUp = (event) => { + if (shouldIgnoreKeyboardEvent(event)) { + return; + } - flock.canvas.addEventListener("keyup", function (event) { flock.canvas.pressedKeys.delete(event.key); - }); + }; + + flock.canvas.addEventListener("keydown", handleKeyDown); + flock.canvas.addEventListener("keyup", handleKeyUp); + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); - flock.canvas.addEventListener("blur", () => { + window.addEventListener("blur", () => { // Clear all pressed keys when window loses focus flock.canvas.pressedKeys.clear(); flock.canvas.pressedButtons.clear(); From 6cafee8978692866e0622cc22c776ea54e77c83d Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Fri, 6 Mar 2026 16:40:24 +0000 Subject: [PATCH 4/5] Expose action key mapping APIs to generated scripts --- flock.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flock.js b/flock.js index 569dce3c..d6b5bc5e 100644 --- a/flock.js +++ b/flock.js @@ -1010,6 +1010,8 @@ export const flock = { setFog: this.setFog?.bind(this), keyPressed: this.keyPressed?.bind(this), actionPressed: this.actionPressed?.bind(this), + setActionKey: this.setActionKey?.bind(this), + getActionKeys: this.getActionKeys?.bind(this), isTouchingSurface: this.isTouchingSurface?.bind(this), meshExists: this.meshExists?.bind(this), seededRandom: this.seededRandom?.bind(this), From 59800d1e4d7a18e864922ac47aa68172b3c89b52 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Fri, 6 Mar 2026 17:08:56 +0000 Subject: [PATCH 5/5] Make action key rebinding override default mappings --- api/sensing.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/api/sensing.js b/api/sensing.js index dadedc4c..c617332c 100644 --- a/api/sensing.js +++ b/api/sensing.js @@ -339,9 +339,11 @@ export const flockSensing = { }; const customKeys = flock.actionKeyMap?.[action] ?? []; - const defaultKeys = defaultActionMap[action] ?? []; + if (customKeys.length) { + return [...new Set(customKeys)]; + } - return [...new Set([...customKeys, ...defaultKeys])]; + return defaultActionMap[action] ?? []; }, setActionKey(action, key) { if (!action || typeof key !== "string") { @@ -355,9 +357,10 @@ export const flockSensing = { // Keep special keys (e.g. ArrowLeft, " ") as-is so runtime checks match // browser keyboard event values. const normalizedKey = /^[a-z]$/i.test(key) ? key.toUpperCase() : key; - const existingKeys = flock.actionKeyMap[action] ?? []; - flock.actionKeyMap[action] = [...new Set([...existingKeys, normalizedKey])]; + // Rebind behavior: setting an action key replaces previous/default bindings + // for that action instead of appending to them. + flock.actionKeyMap[action] = [normalizedKey]; }, actionPressed(action) { const actionKeys = this.getActionKeys(action);