@triflare/fx 2.1.0-beta.3 → 2.1.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflare/fx",
3
- "version": "2.1.0-beta.3",
3
+ "version": "2.1.0-beta.4",
4
4
  "description": "Code PenguinMod projects with text.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/product.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  # Identity
2
2
  name: "Triflare Fx"
3
3
  command: "fx"
4
- version: "2.1.0-beta.3"
4
+ version: "2.1.0-beta.4"
5
5
  tagline: "Code PenguinMod projects with text."
6
6
  author: "Triflare"
7
7
  homepage: "https://github.com/triflare/fx"
package/src/catalog.js CHANGED
@@ -102,6 +102,59 @@ const OF_MENU = {
102
102
  field: "OBJECT",
103
103
  default: "_stage_",
104
104
  };
105
+ const SPRITE_STOP_MENU = {
106
+ type: "menu",
107
+ menu: "control_stop_sprite_menu",
108
+ field: "STOP_OPTION",
109
+ default: "_stage_",
110
+ };
111
+ const VISIBILITY_MENU = {
112
+ type: "menu",
113
+ menu: "looks_changeVisibilityOfSprite_menu",
114
+ field: "VISIBLE_OPTION",
115
+ default: "_myself_",
116
+ };
117
+ const OTHER_VISIBILITY_MENU = {
118
+ type: "menu",
119
+ menu: "looks_getOtherSpriteVisible_menu",
120
+ field: "VISIBLE_OPTION",
121
+ default: "_myself_",
122
+ };
123
+ const COSTUME_INPUT_MENU = {
124
+ type: "menu",
125
+ menu: "looks_getinput_menu",
126
+ field: "INPUT",
127
+ default: "width",
128
+ };
129
+ const FULL_TOUCHING_MENU = {
130
+ type: "menu",
131
+ menu: "sensing_fulltouchingobjectmenu",
132
+ field: "FULLTOUCHINGOBJECTMENU",
133
+ default: "_mouse_",
134
+ };
135
+ const SPRITE_TOUCHING_MENU = {
136
+ type: "menu",
137
+ menu: "sensing_touchingobjectmenusprites",
138
+ field: "SPRITETOUCHINGOBJECTMENU",
139
+ default: "_myself_",
140
+ };
141
+ const SCROLL_MENU = {
142
+ type: "menu",
143
+ menu: "sensing_scrolldirections",
144
+ field: "SCROLL_OPTION",
145
+ default: "up",
146
+ };
147
+ const FINGER_MENU = {
148
+ type: "menu",
149
+ menu: "sensing_fingeroptions",
150
+ field: "FINGER_OPTION",
151
+ default: "1",
152
+ };
153
+
154
+ // Variable and list references are serialized as *fields*, not inputs, but
155
+ // they occupy a positional argument in the Fx call like any other input.
156
+ const VAR_REF = { type: "variable" };
157
+ const LIST_REF = { type: "list" };
105
158
 
106
159
  const CORE_CATALOG = {
107
160
  // ---------------------------------------------------------------- motion
@@ -713,6 +766,867 @@ const CORE_CATALOG = {
713
766
  extension: "fxTempVars",
714
767
  requiresExtension: true,
715
768
  },
769
+ // ---- control (PenguinMod) --------------------------------------------
770
+ "control.backToGreenFlag": {
771
+ opcode: "control_backToGreenFlag",
772
+ },
773
+ "control.continueLoop": {
774
+ opcode: "control_continueLoop",
775
+ },
776
+ "control.decrementCounter": {
777
+ opcode: "control_decr_counter",
778
+ },
779
+ "control.deleteClonesOf": {
780
+ opcode: "control_delete_clones_of",
781
+ inputs: {
782
+ CLONE_OPTION: CLONE_MENU,
783
+ },
784
+ },
785
+ "control.escapeLoop": {
786
+ opcode: "control_exitLoop",
787
+ },
788
+ "control.ifReturnElseReturn": {
789
+ opcode: "control_if_return_else_return",
790
+ kind: "reporter",
791
+ inputs: {
792
+ boolean: BOOLEAN,
793
+ TEXT1: STRING,
794
+ TEXT2: STRING,
795
+ },
796
+ },
797
+ "control.javascriptCommand": {
798
+ opcode: "control_javascript_command",
799
+ inputs: {
800
+ JS: STRING,
801
+ },
802
+ },
803
+ "control.repeatForSeconds": {
804
+ opcode: "control_repeatForSeconds",
805
+ inputs: {
806
+ TIMES: POSITIVE,
807
+ SUBSTACK: SUBSTACK,
808
+ },
809
+ },
810
+ "control.setCounter": {
811
+ opcode: "control_set_counter",
812
+ inputs: {
813
+ VALUE: NUMBER,
814
+ },
815
+ },
816
+ "control.stopSprite": {
817
+ opcode: "control_stop_sprite",
818
+ inputs: {
819
+ STOP_OPTION: SPRITE_STOP_MENU,
820
+ },
821
+ },
822
+ "control.waitSecondsOrUntil": {
823
+ opcode: "control_waitsecondsoruntil",
824
+ inputs: {
825
+ DURATION: POSITIVE,
826
+ CONDITION: BOOLEAN,
827
+ },
828
+ },
829
+ "control.waitTick": {
830
+ opcode: "control_waittick",
831
+ },
832
+
833
+ // ---- data (PenguinMod) --------------------------------------------
834
+ "data.setListFromArray": {
835
+ opcode: "data_arraylist",
836
+ inputs: {
837
+ LIST: LIST_REF,
838
+ VALUE: STRING,
839
+ },
840
+ },
841
+ "data.itemExists": {
842
+ opcode: "data_itemexistslist",
843
+ kind: "boolean",
844
+ inputs: {
845
+ INDEX: WHOLE,
846
+ LIST: LIST_REF,
847
+ },
848
+ },
849
+ "data.listAsArray": {
850
+ opcode: "data_listarray",
851
+ kind: "reporter",
852
+ inputs: {
853
+ LIST: LIST_REF,
854
+ },
855
+ },
856
+ "data.forEachItem": {
857
+ opcode: "data_listforeachitem",
858
+ inputs: {
859
+ LIST: LIST_REF,
860
+ VARIABLE: VAR_REF,
861
+ SUBSTACK: SUBSTACK,
862
+ },
863
+ },
864
+ "data.forEachIndex": {
865
+ opcode: "data_listforeachnum",
866
+ inputs: {
867
+ LIST: LIST_REF,
868
+ VARIABLE: VAR_REF,
869
+ SUBSTACK: SUBSTACK,
870
+ },
871
+ },
872
+ "data.listIsEmpty": {
873
+ opcode: "data_listisempty",
874
+ kind: "boolean",
875
+ inputs: {
876
+ LIST: LIST_REF,
877
+ },
878
+ },
879
+ "data.reverseList": {
880
+ opcode: "data_reverselist",
881
+ inputs: {
882
+ LIST: LIST_REF,
883
+ },
884
+ },
885
+
886
+ // ---- events (PenguinMod) --------------------------------------------
887
+ "events.always": {
888
+ opcode: "event_always",
889
+ kind: "hat",
890
+ args: [],
891
+ },
892
+ "events.whenAnything": {
893
+ opcode: "event_whenanything",
894
+ kind: "hat",
895
+ args: [{ input: "ANYTHING", type: "boolean" }],
896
+ },
897
+ "events.whenJavaScript": {
898
+ opcode: "event_whenjavascript",
899
+ kind: "hat",
900
+ args: [{ input: "JS", type: "string" }],
901
+ },
902
+ "events.whenKeyHit": {
903
+ opcode: "event_whenkeyhit",
904
+ kind: "hat",
905
+ args: [{ field: "KEY_OPTION", default: "space" }],
906
+ },
907
+ "events.whenMouseScrolled": {
908
+ opcode: "event_whenmousescrolled",
909
+ kind: "hat",
910
+ args: [{ field: "KEY_OPTION", default: "up" }],
911
+ },
912
+ "events.whenStopClicked": {
913
+ opcode: "event_whenstopclicked",
914
+ kind: "hat",
915
+ args: [],
916
+ },
917
+
918
+ // ---- looks (PenguinMod) --------------------------------------------
919
+ "looks.changeStretch": {
920
+ opcode: "looks_changeStretch",
921
+ inputs: {
922
+ X: NUMBER,
923
+ Y: NUMBER,
924
+ },
925
+ },
926
+ "looks.changeVisibilityOf": {
927
+ opcode: "looks_changeVisibilityOfSprite",
928
+ fields: { VISIBLE_TYPE: "show" },
929
+ inputs: {
930
+ VISIBLE_OPTION: VISIBILITY_MENU,
931
+ },
932
+ },
933
+ "looks.hideSprite": {
934
+ opcode: "looks_changeVisibilityOfSpriteHide",
935
+ inputs: {
936
+ VISIBLE_OPTION: VISIBILITY_MENU,
937
+ },
938
+ },
939
+ "looks.showSprite": {
940
+ opcode: "looks_changeVisibilityOfSpriteShow",
941
+ inputs: {
942
+ VISIBLE_OPTION: VISIBILITY_MENU,
943
+ },
944
+ },
945
+ "looks.effectValue": {
946
+ opcode: "looks_getEffectValue",
947
+ kind: "reporter",
948
+ fields: { EFFECT: "COLOR" },
949
+ },
950
+ "looks.isSpriteVisible": {
951
+ opcode: "looks_getOtherSpriteVisible",
952
+ kind: "boolean",
953
+ inputs: {
954
+ VISIBLE_OPTION: OTHER_VISIBILITY_MENU,
955
+ },
956
+ },
957
+ "looks.isVisible": {
958
+ opcode: "looks_getSpriteVisible",
959
+ kind: "boolean",
960
+ },
961
+ "looks.costumeInput": {
962
+ opcode: "looks_getinputofcostume",
963
+ kind: "reporter",
964
+ inputs: {
965
+ INPUT: COSTUME_INPUT_MENU,
966
+ COSTUME: COSTUME_MENU,
967
+ },
968
+ },
969
+ "looks.goToTargetLayer": {
970
+ opcode: "looks_goTargetLayer",
971
+ fields: { FORWARD_BACKWARD: "infront" },
972
+ inputs: {
973
+ VISIBLE_OPTION: OTHER_VISIBILITY_MENU,
974
+ },
975
+ },
976
+ "looks.layer": {
977
+ opcode: "looks_layersGetLayer",
978
+ kind: "reporter",
979
+ },
980
+ "looks.setLayer": {
981
+ opcode: "looks_layersSetLayer",
982
+ inputs: {
983
+ NUM: INTEGER,
984
+ },
985
+ },
986
+ "looks.previousBackdrop": {
987
+ opcode: "looks_previousbackdrop",
988
+ },
989
+ "looks.previousCostume": {
990
+ opcode: "looks_previouscostume",
991
+ },
992
+ "looks.sayHeight": {
993
+ opcode: "looks_sayHeight",
994
+ kind: "reporter",
995
+ },
996
+ "looks.sayWidth": {
997
+ opcode: "looks_sayWidth",
998
+ kind: "reporter",
999
+ },
1000
+ "looks.setBubbleColor": {
1001
+ opcode: "looks_setColor",
1002
+ fields: { prop: "BUBBLE_STROKE" },
1003
+ inputs: {
1004
+ color: COLOR,
1005
+ },
1006
+ },
1007
+ "looks.setFont": {
1008
+ opcode: "looks_setFont",
1009
+ inputs: {
1010
+ font: { type: "string", default: "Helvetica" },
1011
+ size: { type: "number", default: "14" },
1012
+ },
1013
+ },
1014
+ "looks.setTextBubbleShape": {
1015
+ opcode: "looks_setShape",
1016
+ fields: { prop: "MIN_WIDTH" },
1017
+ inputs: {
1018
+ color: NUMBER,
1019
+ },
1020
+ },
1021
+ "looks.setStretch": {
1022
+ opcode: "looks_setStretch",
1023
+ inputs: {
1024
+ X: NUMBER,
1025
+ Y: NUMBER,
1026
+ },
1027
+ },
1028
+ "looks.setTintColor": {
1029
+ opcode: "looks_setTintColor",
1030
+ inputs: {
1031
+ color: COLOR,
1032
+ },
1033
+ },
1034
+ "looks.stopTalking": {
1035
+ opcode: "looks_stoptalking",
1036
+ },
1037
+ "looks.stretchX": {
1038
+ opcode: "looks_stretchGetX",
1039
+ kind: "reporter",
1040
+ },
1041
+ "looks.stretchY": {
1042
+ opcode: "looks_stretchGetY",
1043
+ kind: "reporter",
1044
+ },
1045
+ "looks.tintColor": {
1046
+ opcode: "looks_tintColor",
1047
+ kind: "reporter",
1048
+ },
1049
+
1050
+ // ---- motion (PenguinMod) --------------------------------------------
1051
+ "motion.changeByXY": {
1052
+ opcode: "motion_changebyxy",
1053
+ inputs: {
1054
+ DX: NUMBER,
1055
+ DY: NUMBER,
1056
+ },
1057
+ },
1058
+ "motion.ifOnSpriteBounce": {
1059
+ opcode: "motion_ifonspritebounce",
1060
+ inputs: {
1061
+ SPRITE: POINT_MENU,
1062
+ },
1063
+ },
1064
+ "motion.ifOnXYBounce": {
1065
+ opcode: "motion_ifonxybounce",
1066
+ inputs: {
1067
+ X: NUMBER,
1068
+ Y: NUMBER,
1069
+ },
1070
+ },
1071
+ "motion.moveToStageSide": {
1072
+ opcode: "motion_move_sprite_to_scene_side",
1073
+ fields: { ALIGNMENT: "bottom-left" },
1074
+ },
1075
+ "motion.moveBackSteps": {
1076
+ opcode: "motion_movebacksteps",
1077
+ inputs: {
1078
+ STEPS: NUMBER,
1079
+ },
1080
+ },
1081
+ "motion.moveUpDownSteps": {
1082
+ opcode: "motion_moveupdownsteps",
1083
+ fields: { DIRECTION: "up" },
1084
+ inputs: {
1085
+ STEPS: NUMBER,
1086
+ },
1087
+ },
1088
+ "motion.pointInRandomDirection": {
1089
+ opcode: "motion_pointinrandomdirection",
1090
+ },
1091
+ "motion.pointTowardsXY": {
1092
+ opcode: "motion_pointtowardsxy",
1093
+ inputs: {
1094
+ X: NUMBER,
1095
+ Y: NUMBER,
1096
+ },
1097
+ },
1098
+ "motion.turnAround": {
1099
+ opcode: "motion_turnaround",
1100
+ },
1101
+ "motion.turnLeftAroundXY": {
1102
+ opcode: "motion_turnleftaroundxy",
1103
+ inputs: {
1104
+ DEGREES: NUMBER,
1105
+ X: NUMBER,
1106
+ Y: NUMBER,
1107
+ },
1108
+ },
1109
+ "motion.turnRightAroundXY": {
1110
+ opcode: "motion_turnrightaroundxy",
1111
+ inputs: {
1112
+ DEGREES: NUMBER,
1113
+ X: NUMBER,
1114
+ Y: NUMBER,
1115
+ },
1116
+ },
1117
+
1118
+ // ---- operators (PenguinMod) --------------------------------------------
1119
+ "operators.advancedMath": {
1120
+ opcode: "operator_advMath",
1121
+ kind: "reporter",
1122
+ fields: { OPTION: "^" },
1123
+ inputs: {
1124
+ ONE: NUMBER,
1125
+ TWO: NUMBER,
1126
+ },
1127
+ },
1128
+ "operators.advancedMathExpanded": {
1129
+ opcode: "operator_advMathExpanded",
1130
+ kind: "reporter",
1131
+ fields: { OPTION: "root" },
1132
+ inputs: {
1133
+ ONE: NUMBER,
1134
+ TWO: NUMBER,
1135
+ THREE: NUMBER,
1136
+ },
1137
+ },
1138
+ "operators.advancedLog": {
1139
+ opcode: "operator_advlog",
1140
+ kind: "reporter",
1141
+ inputs: {
1142
+ NUM1: NUMBER,
1143
+ NUM2: NUMBER,
1144
+ },
1145
+ },
1146
+ "operators.boolify": {
1147
+ opcode: "operator_boolify",
1148
+ kind: "boolean",
1149
+ inputs: {
1150
+ ONE: STRING,
1151
+ },
1152
+ },
1153
+ "operators.characterToCode": {
1154
+ opcode: "operator_character_to_code",
1155
+ kind: "reporter",
1156
+ inputs: {
1157
+ ONE: STRING,
1158
+ },
1159
+ },
1160
+ "operators.codeToCharacter": {
1161
+ opcode: "operator_code_to_character",
1162
+ kind: "reporter",
1163
+ inputs: {
1164
+ ONE: NUMBER,
1165
+ },
1166
+ },
1167
+ "operators.constrain": {
1168
+ opcode: "operator_constrainnumber",
1169
+ kind: "reporter",
1170
+ inputs: {
1171
+ inp: NUMBER,
1172
+ min: NUMBER,
1173
+ max: NUMBER,
1174
+ },
1175
+ },
1176
+ "operators.countAppearances": {
1177
+ opcode: "operator_countAppearTimes",
1178
+ kind: "reporter",
1179
+ inputs: {
1180
+ TEXT1: STRING,
1181
+ TEXT2: STRING,
1182
+ },
1183
+ },
1184
+ "operators.falseValue": {
1185
+ opcode: "operator_falseBoolean",
1186
+ kind: "boolean",
1187
+ },
1188
+ "operators.lettersBetween": {
1189
+ opcode: "operator_getLettersFromIndexToIndexInText",
1190
+ kind: "reporter",
1191
+ inputs: {
1192
+ INDEX1: NUMBER,
1193
+ INDEX2: NUMBER,
1194
+ TEXT: STRING,
1195
+ },
1196
+ },
1197
+ "operators.lettersBetweenFixed": {
1198
+ opcode: "operator_getLettersFromIndexToIndexInTextFixed",
1199
+ kind: "reporter",
1200
+ inputs: {
1201
+ INDEX1: NUMBER,
1202
+ INDEX2: NUMBER,
1203
+ TEXT: STRING,
1204
+ },
1205
+ },
1206
+ "operators.gte": {
1207
+ opcode: "operator_gtorequal",
1208
+ kind: "boolean",
1209
+ inputs: {
1210
+ OPERAND1: STRING,
1211
+ OPERAND2: STRING,
1212
+ },
1213
+ },
1214
+ "operators.indexOf": {
1215
+ opcode: "operator_indexOfTextInText",
1216
+ kind: "reporter",
1217
+ inputs: {
1218
+ TEXT1: STRING,
1219
+ TEXT2: STRING,
1220
+ },
1221
+ },
1222
+ "operators.javascriptBoolean": {
1223
+ opcode: "operator_javascript_boolean",
1224
+ kind: "boolean",
1225
+ inputs: {
1226
+ JS: STRING,
1227
+ },
1228
+ },
1229
+ "operators.javascriptOutput": {
1230
+ opcode: "operator_javascript_output",
1231
+ kind: "reporter",
1232
+ inputs: {
1233
+ JS: STRING,
1234
+ },
1235
+ },
1236
+ "operators.join3": {
1237
+ opcode: "operator_join3",
1238
+ kind: "reporter",
1239
+ inputs: {
1240
+ STRING1: STRING,
1241
+ STRING2: STRING,
1242
+ STRING3: STRING,
1243
+ },
1244
+ },
1245
+ "operators.lastIndexOf": {
1246
+ opcode: "operator_lastIndexOfTextInText",
1247
+ kind: "reporter",
1248
+ inputs: {
1249
+ TEXT1: STRING,
1250
+ TEXT2: STRING,
1251
+ },
1252
+ },
1253
+ "operators.interpolate": {
1254
+ opcode: "operator_lerpFunc",
1255
+ kind: "reporter",
1256
+ inputs: {
1257
+ ONE: NUMBER,
1258
+ TWO: NUMBER,
1259
+ AMOUNT: NUMBER,
1260
+ },
1261
+ },
1262
+ "operators.lte": {
1263
+ opcode: "operator_ltorequal",
1264
+ kind: "boolean",
1265
+ inputs: {
1266
+ OPERAND1: STRING,
1267
+ OPERAND2: STRING,
1268
+ },
1269
+ },
1270
+ "operators.nand": {
1271
+ opcode: "operator_nand",
1272
+ kind: "boolean",
1273
+ inputs: {
1274
+ OPERAND1: BOOLEAN,
1275
+ OPERAND2: BOOLEAN,
1276
+ },
1277
+ },
1278
+ "operators.newLine": {
1279
+ opcode: "operator_newLine",
1280
+ kind: "reporter",
1281
+ },
1282
+ "operators.nor": {
1283
+ opcode: "operator_nor",
1284
+ kind: "boolean",
1285
+ inputs: {
1286
+ OPERAND1: BOOLEAN,
1287
+ OPERAND2: BOOLEAN,
1288
+ },
1289
+ },
1290
+ "operators.notEqual": {
1291
+ opcode: "operator_notequal",
1292
+ kind: "boolean",
1293
+ inputs: {
1294
+ OPERAND1: STRING,
1295
+ OPERAND2: STRING,
1296
+ },
1297
+ },
1298
+ "operators.power": {
1299
+ opcode: "operator_power",
1300
+ kind: "reporter",
1301
+ inputs: {
1302
+ NUM1: NUMBER,
1303
+ NUM2: NUMBER,
1304
+ },
1305
+ },
1306
+ "operators.randomBoolean": {
1307
+ opcode: "operator_randomBoolean",
1308
+ kind: "boolean",
1309
+ },
1310
+ "operators.readLine": {
1311
+ opcode: "operator_readLineInMultilineText",
1312
+ kind: "reporter",
1313
+ inputs: {
1314
+ LINE: NUMBER,
1315
+ TEXT: STRING,
1316
+ },
1317
+ },
1318
+ "operators.regexMatch": {
1319
+ opcode: "operator_regexmatch",
1320
+ kind: "reporter",
1321
+ inputs: {
1322
+ text: STRING,
1323
+ reg: STRING,
1324
+ regrule: STRING,
1325
+ },
1326
+ },
1327
+ "operators.replaceAll": {
1328
+ opcode: "operator_replaceAll",
1329
+ kind: "reporter",
1330
+ inputs: {
1331
+ text: STRING,
1332
+ term: STRING,
1333
+ res: STRING,
1334
+ },
1335
+ },
1336
+ "operators.replaceFirst": {
1337
+ opcode: "operator_replaceFirst",
1338
+ kind: "reporter",
1339
+ inputs: {
1340
+ text: STRING,
1341
+ term: STRING,
1342
+ res: STRING,
1343
+ },
1344
+ },
1345
+ "operators.stringify": {
1346
+ opcode: "operator_stringify",
1347
+ kind: "reporter",
1348
+ inputs: {
1349
+ ONE: STRING,
1350
+ },
1351
+ },
1352
+ "operators.tabCharacter": {
1353
+ opcode: "operator_tabCharacter",
1354
+ kind: "reporter",
1355
+ },
1356
+ "operators.includesLetterFrom": {
1357
+ opcode: "operator_textIncludesLetterFrom",
1358
+ kind: "boolean",
1359
+ inputs: {
1360
+ TEXT1: STRING,
1361
+ TEXT2: STRING,
1362
+ },
1363
+ },
1364
+ "operators.startsWithOrEndsWith": {
1365
+ opcode: "operator_textStartsOrEndsWith",
1366
+ kind: "boolean",
1367
+ fields: { OPTION: "starts" },
1368
+ inputs: {
1369
+ TEXT1: STRING,
1370
+ TEXT2: STRING,
1371
+ },
1372
+ },
1373
+ "operators.toCase": {
1374
+ opcode: "operator_toUpperLowerCase",
1375
+ kind: "reporter",
1376
+ fields: { OPTION: "upper" },
1377
+ inputs: {
1378
+ TEXT: STRING,
1379
+ },
1380
+ },
1381
+ "operators.trueValue": {
1382
+ opcode: "operator_trueBoolean",
1383
+ kind: "boolean",
1384
+ },
1385
+ "operators.xnor": {
1386
+ opcode: "operator_xnor",
1387
+ kind: "boolean",
1388
+ inputs: {
1389
+ OPERAND1: STRING,
1390
+ OPERAND2: STRING,
1391
+ },
1392
+ },
1393
+ "operators.xor": {
1394
+ opcode: "operator_xor",
1395
+ kind: "boolean",
1396
+ inputs: {
1397
+ OPERAND1: STRING,
1398
+ OPERAND2: STRING,
1399
+ },
1400
+ },
1401
+
1402
+ // ---- sensing (PenguinMod) --------------------------------------------
1403
+ "sensing.directionBetween": {
1404
+ opcode: "sensing_directionTo",
1405
+ kind: "reporter",
1406
+ inputs: {
1407
+ x2: STRING,
1408
+ y2: STRING,
1409
+ x1: STRING,
1410
+ y1: STRING,
1411
+ },
1412
+ },
1413
+ "sensing.distanceBetween": {
1414
+ opcode: "sensing_distanceTo",
1415
+ kind: "reporter",
1416
+ inputs: {
1417
+ x1: STRING,
1418
+ y1: STRING,
1419
+ x2: STRING,
1420
+ y2: STRING,
1421
+ },
1422
+ },
1423
+ "sensing.fingerDown": {
1424
+ opcode: "sensing_fingerdown",
1425
+ kind: "boolean",
1426
+ inputs: {
1427
+ FINGER_OPTION: FINGER_MENU,
1428
+ },
1429
+ },
1430
+ "sensing.fingerTapped": {
1431
+ opcode: "sensing_fingertapped",
1432
+ kind: "boolean",
1433
+ inputs: {
1434
+ FINGER_OPTION: FINGER_MENU,
1435
+ },
1436
+ },
1437
+ "sensing.fingerX": {
1438
+ opcode: "sensing_fingerx",
1439
+ kind: "reporter",
1440
+ inputs: {
1441
+ FINGER_OPTION: FINGER_MENU,
1442
+ },
1443
+ },
1444
+ "sensing.fingerY": {
1445
+ opcode: "sensing_fingery",
1446
+ kind: "reporter",
1447
+ inputs: {
1448
+ FINGER_OPTION: FINGER_MENU,
1449
+ },
1450
+ },
1451
+ "sensing.browser": {
1452
+ opcode: "sensing_getbrowser",
1453
+ kind: "reporter",
1454
+ },
1455
+ "sensing.clipboard": {
1456
+ opcode: "sensing_getclipboard",
1457
+ kind: "reporter",
1458
+ },
1459
+ "sensing.isDraggable": {
1460
+ opcode: "sensing_getdragmode",
1461
+ kind: "boolean",
1462
+ },
1463
+ "sensing.operatingSystem": {
1464
+ opcode: "sensing_getoperatingsystem",
1465
+ kind: "reporter",
1466
+ },
1467
+ "sensing.spriteWithAttribute": {
1468
+ opcode: "sensing_getspritewithattrib",
1469
+ kind: "reporter",
1470
+ inputs: {
1471
+ var: STRING,
1472
+ val: STRING,
1473
+ },
1474
+ },
1475
+ "sensing.url": {
1476
+ opcode: "sensing_geturl",
1477
+ kind: "reporter",
1478
+ },
1479
+ "sensing.touchingSpritePoint": {
1480
+ opcode: "sensing_getxyoftouchingsprite",
1481
+ kind: "reporter",
1482
+ fields: { XY: "x" },
1483
+ inputs: {
1484
+ SPRITE: DISTANCE_MENU,
1485
+ },
1486
+ },
1487
+ "sensing.isUpperCase": {
1488
+ opcode: "sensing_isUpperCase",
1489
+ kind: "boolean",
1490
+ inputs: {
1491
+ text: STRING,
1492
+ },
1493
+ },
1494
+ "sensing.keyHit": {
1495
+ opcode: "sensing_keyhit",
1496
+ kind: "boolean",
1497
+ inputs: {
1498
+ KEY_OPTION: KEY_MENU,
1499
+ },
1500
+ },
1501
+ "sensing.loggedIn": {
1502
+ opcode: "sensing_loggedin",
1503
+ kind: "boolean",
1504
+ },
1505
+ "sensing.isMobile": {
1506
+ opcode: "sensing_mobile",
1507
+ kind: "boolean",
1508
+ },
1509
+ "sensing.mouseClicked": {
1510
+ opcode: "sensing_mouseclicked",
1511
+ kind: "boolean",
1512
+ },
1513
+ "sensing.mouseScrolling": {
1514
+ opcode: "sensing_mousescrolling",
1515
+ kind: "boolean",
1516
+ inputs: {
1517
+ SCROLL_OPTION: SCROLL_MENU,
1518
+ },
1519
+ },
1520
+ "sensing.objectTouchingClone": {
1521
+ opcode: "sensing_objecttouchingclonesprite",
1522
+ kind: "boolean",
1523
+ inputs: {
1524
+ FULLTOUCHINGOBJECTMENU: FULL_TOUCHING_MENU,
1525
+ SPRITETOUCHINGOBJECTMENU: SPRITE_TOUCHING_MENU,
1526
+ },
1527
+ },
1528
+ "sensing.objectTouchingObject": {
1529
+ opcode: "sensing_objecttouchingobject",
1530
+ kind: "boolean",
1531
+ inputs: {
1532
+ FULLTOUCHINGOBJECTMENU: FULL_TOUCHING_MENU,
1533
+ SPRITETOUCHINGOBJECTMENU: SPRITE_TOUCHING_MENU,
1534
+ },
1535
+ },
1536
+ "sensing.regexTest": {
1537
+ opcode: "sensing_regextest",
1538
+ kind: "boolean",
1539
+ inputs: {
1540
+ text: STRING,
1541
+ reg: STRING,
1542
+ regrule: STRING,
1543
+ },
1544
+ },
1545
+ "sensing.setClipboard": {
1546
+ opcode: "sensing_setclipboard",
1547
+ inputs: {
1548
+ ITEM: STRING,
1549
+ },
1550
+ },
1551
+ "sensing.hasNumber": {
1552
+ opcode: "sensing_thing_has_number",
1553
+ kind: "boolean",
1554
+ inputs: {
1555
+ TEXT1: STRING,
1556
+ },
1557
+ },
1558
+ "sensing.isNumber": {
1559
+ opcode: "sensing_thing_is_number",
1560
+ kind: "boolean",
1561
+ inputs: {
1562
+ TEXT1: STRING,
1563
+ },
1564
+ },
1565
+ "sensing.isText": {
1566
+ opcode: "sensing_thing_is_text",
1567
+ kind: "boolean",
1568
+ inputs: {
1569
+ TEXT1: STRING,
1570
+ },
1571
+ },
1572
+
1573
+ // ---- sound (PenguinMod) --------------------------------------------
1574
+ "sound.effectValue": {
1575
+ opcode: "sound_getEffectValue",
1576
+ kind: "reporter",
1577
+ fields: { EFFECT: "PITCH" },
1578
+ },
1579
+ "sound.lengthOf": {
1580
+ opcode: "sound_getLength",
1581
+ kind: "reporter",
1582
+ inputs: {
1583
+ SOUND_MENU: SOUND_MENU,
1584
+ },
1585
+ },
1586
+ "sound.soundVolume": {
1587
+ opcode: "sound_getSoundVolume",
1588
+ kind: "reporter",
1589
+ inputs: {
1590
+ SOUND_MENU: SOUND_MENU,
1591
+ },
1592
+ },
1593
+ "sound.isPlaying": {
1594
+ opcode: "sound_isSoundPlaying",
1595
+ kind: "boolean",
1596
+ inputs: {
1597
+ SOUND_MENU: SOUND_MENU,
1598
+ },
1599
+ },
1600
+ "sound.playAt": {
1601
+ opcode: "sound_play_at_seconds",
1602
+ inputs: {
1603
+ SOUND_MENU: SOUND_MENU,
1604
+ VALUE: NUMBER,
1605
+ },
1606
+ },
1607
+ "sound.playAtUntilDone": {
1608
+ opcode: "sound_play_at_seconds_until_done",
1609
+ inputs: {
1610
+ SOUND_MENU: SOUND_MENU,
1611
+ VALUE: NUMBER,
1612
+ },
1613
+ },
1614
+ "sound.playAllSounds": {
1615
+ opcode: "sound_playallsounds",
1616
+ },
1617
+ "sound.setFadeOut": {
1618
+ opcode: "sound_set_stop_fadeout_to",
1619
+ inputs: {
1620
+ VALUE: NUMBER,
1621
+ SOUND_MENU: SOUND_MENU,
1622
+ },
1623
+ },
1624
+ "sound.stop": {
1625
+ opcode: "sound_stop",
1626
+ inputs: {
1627
+ SOUND_MENU: SOUND_MENU,
1628
+ },
1629
+ },
716
1630
  };
717
1631
 
718
1632
  /**
@@ -1,15 +1,104 @@
1
- import { CATALOG, blockNames } from "../../catalog.js";
1
+ import { CATALOG } from "../../catalog.js";
2
2
  import { loadProjectState } from "../context.js";
3
+ import { plural, terminalColumns } from "../format.js";
4
+
5
+ // Language order for the built-in categories; extension groups follow
6
+ // alphabetically so their blocks stay together at the end of the listing.
7
+ const CATEGORY_ORDER = [
8
+ "motion",
9
+ "looks",
10
+ "sound",
11
+ "sensing",
12
+ "operators",
13
+ "control",
14
+ "events",
15
+ "data",
16
+ ];
17
+
18
+ // One-character kind marker, so a name alone still says how the block is used.
19
+ const KIND_MARKS = {
20
+ command: " ",
21
+ reporter: "→",
22
+ boolean: "?",
23
+ hat: "▸",
24
+ };
25
+
26
+ /** `SCREAMING_SNAKE_CASE` name for a catalog argument. */
27
+ function argumentName(name) {
28
+ return name
29
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
30
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
31
+ .toUpperCase();
32
+ }
33
+
34
+ /** Positional arguments a block takes, in the order the compiler fills them. */
35
+ function argumentNames(entry) {
36
+ // `pm.*` are argument reporters whose VALUE field is the reporter's own
37
+ // label ("is compiled?"), not a parameter the caller supplies.
38
+ if (entry.opcode?.startsWith("argument_reporter")) return [];
39
+ if (entry.kind === "hat")
40
+ return (entry.args || []).map((arg) => arg.field || arg.input);
41
+ return [
42
+ ...Object.keys(entry.fields || {}),
43
+ ...Object.entries(entry.inputs || {})
44
+ .filter(([, spec]) => spec.type !== "substack")
45
+ .map(([name]) => name),
46
+ ];
47
+ }
48
+
49
+ /** `category.block(ARG, ...)` call shape shown in the listing. */
50
+ function signature(name, entry) {
51
+ const args = argumentNames(entry).map(argumentName);
52
+ return `${name}(${args.join(", ")})`;
53
+ }
54
+
55
+ function mark(entry) {
56
+ return KIND_MARKS[entry.kind] ?? " ";
57
+ }
58
+
59
+ /** Group block names by category, in reading order. */
60
+ function groupNames(names) {
61
+ const groups = new Map();
62
+ for (const name of names) {
63
+ const group = name.split(".")[0];
64
+ if (!groups.has(group)) groups.set(group, []);
65
+ groups.get(group).push(name);
66
+ }
67
+ const rank = (group) => {
68
+ const index = CATEGORY_ORDER.indexOf(group);
69
+ return index < 0 ? CATEGORY_ORDER.length : index;
70
+ };
71
+ return new Map(
72
+ [...groups.entries()].sort(
73
+ ([left], [right]) =>
74
+ rank(left) - rank(right) || left.localeCompare(right),
75
+ ),
76
+ );
77
+ }
3
78
 
4
79
  export const catalogCommand = {
5
80
  name: "catalog",
6
81
  aliases: ["l", "blocks"],
7
82
  group: "Core Commands:",
8
83
  summary: "List available blocks",
9
- description: `List block names, including blocks from extensions configured in the
10
- project.`,
84
+ description: `List block names grouped by category, including blocks from extensions
85
+ configured in the project.`,
11
86
  arguments: ["[dir]"],
12
- examples: ["fx catalog", "fx catalog --json"],
87
+ options: [
88
+ {
89
+ flags: "--category <name>",
90
+ description: "Only list one category, such as motion or pen",
91
+ },
92
+ {
93
+ flags: "--search <text>",
94
+ description: "Only list blocks whose name contains the text",
95
+ },
96
+ ],
97
+ examples: [
98
+ "fx catalog",
99
+ "fx catalog --category motion",
100
+ "fx catalog --search glide",
101
+ ],
13
102
  async run({ args, cwd, options, reporter, product, prompts }) {
14
103
  const state = await loadProjectState({
15
104
  input: args[0],
@@ -19,24 +108,69 @@ project.`,
19
108
  prompts,
20
109
  });
21
110
  const merged = { ...CATALOG, ...state.catalog };
22
- const names = Object.keys(merged).sort();
23
- const builtIn = blockNames().length;
24
-
25
- reporter.heading(
26
- `${product.name} catalog (${names.length} blocks` +
27
- (names.length > builtIn
28
- ? `, ${names.length - builtIn} from extensions`
29
- : "") +
30
- "):",
111
+ const category =
112
+ typeof options.category === "string"
113
+ ? options.category.toLowerCase()
114
+ : null;
115
+ const search =
116
+ typeof options.search === "string" ? options.search.toLowerCase() : null;
117
+ const names = Object.keys(merged)
118
+ .filter(
119
+ (name) => !category || name.split(".")[0].toLowerCase() === category,
120
+ )
121
+ .filter((name) => !search || name.toLowerCase().includes(search))
122
+ .sort();
123
+ const selected = Object.fromEntries(
124
+ names.map((name) => [name, merged[name]]),
31
125
  );
32
- for (const name of names) {
33
- const entry = merged[name];
34
- reporter.item(name, `-> ${entry.opcode} [${entry.kind}]`, {
35
- width: 32,
36
- suffix: entry.extension ? `(ext ${entry.extension})` : "",
37
- });
126
+
127
+ if (names.length === 0) {
128
+ const filters = [
129
+ category && `--category "${options.category}"`,
130
+ search && `--search "${options.search}"`,
131
+ ]
132
+ .filter(Boolean)
133
+ .join(" and ");
134
+ reporter.info(`No blocks match ${filters}.`);
135
+ reporter.hint(`run "${product.command} catalog" to see every block`);
136
+ return selected;
137
+ }
138
+
139
+ const groups = groupNames(names);
140
+ let title = `${product.name} catalog — ${plural(names.length, "block")}`;
141
+ if (category) title += ` · ${category}`;
142
+ else if (search) title += ` matching "${options.search}"`;
143
+ else title += ` in ${plural(groups.size, "group")}`;
144
+ reporter.heading(title);
145
+ if (!category && !search)
146
+ reporter.hint("narrow with --category <name> or --search <text>");
147
+
148
+ const width = terminalColumns();
149
+ for (const [group, entries] of groups) {
150
+ reporter.text("");
151
+ reporter.item(group, "", { width: 0, suffix: `· ${entries.length}` });
152
+ if (reporter.verbose) {
153
+ for (const name of entries) {
154
+ const entry = merged[name];
155
+ reporter.item(
156
+ `${mark(entry)} ${signature(name, entry)}`,
157
+ `-> ${entry.opcode} [${entry.kind}]`,
158
+ {
159
+ width: 44,
160
+ suffix: entry.extension ? `(ext ${entry.extension})` : "",
161
+ },
162
+ );
163
+ }
164
+ } else {
165
+ reporter.columns(
166
+ entries.map(
167
+ (name) => `${mark(merged[name])} ${signature(name, merged[name])}`,
168
+ ),
169
+ { width },
170
+ );
171
+ }
38
172
  }
39
173
 
40
- return merged;
174
+ return selected;
41
175
  },
42
176
  };
package/src/cli/format.js CHANGED
@@ -29,3 +29,55 @@ export function truncate(text, max) {
29
29
  const value = String(text);
30
30
  return value.length > max ? `${value.slice(0, max)}…` : value;
31
31
  }
32
+
33
+ /** Usable terminal width, clamped so narrow and huge terminals stay readable. */
34
+ export function terminalColumns(
35
+ stream = process.stdout,
36
+ { fallback = 80, min = 40, max = 120 } = {},
37
+ ) {
38
+ const width = stream?.columns;
39
+ if (!Number.isFinite(width) || width <= 0) return fallback;
40
+ return Math.min(max, Math.max(min, width));
41
+ }
42
+
43
+ /**
44
+ * Lay items out in aligned columns that fit `width`. Entries wider than one
45
+ * cell get a line of their own, so a single long name does not collapse the
46
+ * whole listing into one column. Trailing whitespace is removed.
47
+ */
48
+ export function formatColumns(
49
+ items,
50
+ { width = 80, indent = 2, gap = 2, cellWidth = 38 } = {},
51
+ ) {
52
+ if (items.length === 0) return [];
53
+ const available = Math.max(width - indent, 1);
54
+ const columns = Math.max(
55
+ 1,
56
+ Math.min(4, Math.floor((available + gap) / (cellWidth + gap))),
57
+ );
58
+ const cell = Math.floor((available - gap * (columns - 1)) / columns);
59
+ const lines = [];
60
+ let row = [];
61
+
62
+ const flush = () => {
63
+ if (row.length === 0) return;
64
+ const cells = row.map((text, index) =>
65
+ index === row.length - 1 ? text : text.padEnd(cell),
66
+ );
67
+ lines.push(`${" ".repeat(indent)}${cells.join(" ".repeat(gap))}`.trimEnd());
68
+ row = [];
69
+ };
70
+
71
+ for (const item of items) {
72
+ const text = String(item);
73
+ if (text.length > cell) {
74
+ flush();
75
+ lines.push(`${" ".repeat(indent)}${text}`);
76
+ continue;
77
+ }
78
+ row.push(text);
79
+ if (row.length === columns) flush();
80
+ }
81
+ flush();
82
+ return lines;
83
+ }
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { styleText } from "node:util";
10
10
  import { brandText } from "../brand.js";
11
+ import { formatColumns } from "./format.js";
11
12
  import { createTheme } from "./theme.js";
12
13
 
13
14
  const STATUS = {
@@ -145,6 +146,13 @@ export function createReporter({
145
146
  stdout.write(` ${theme.accent(padded)}${detail}${note}\n`);
146
147
  },
147
148
 
149
+ /** Compact multi-column rows for long block and file listings. */
150
+ columns(items, { width = 80, indent = 2, gap = 2 } = {}) {
151
+ if (!human) return;
152
+ for (const line of formatColumns(items, { width, indent, gap }))
153
+ stdout.write(`${line}\n`);
154
+ },
155
+
148
156
  /** Plain block of text with no decoration. */
149
157
  text(message) {
150
158
  if (!human) return;
package/src/compiler.js CHANGED
@@ -45,6 +45,8 @@ const NUMBER_TYPES = {
45
45
  const TERMINAL_OPCODES = new Set([
46
46
  "procedures_return",
47
47
  "control_delete_this_clone",
48
+ "control_exitLoop",
49
+ "control_continueLoop",
48
50
  ]);
49
51
 
50
52
  let seed = 0n;
@@ -778,6 +780,9 @@ export class Compiler {
778
780
  if (block.opcode === "procedures_return") return "`return`";
779
781
  if (block.opcode === "control_delete_this_clone")
780
782
  return "`control.deleteThisClone()`";
783
+ if (block.opcode === "control_exitLoop") return "`control.escapeLoop()`";
784
+ if (block.opcode === "control_continueLoop")
785
+ return "`control.continueLoop()`";
781
786
  if (block.opcode === "control_stop") {
782
787
  const option = block.fields.STOP_OPTION
783
788
  ? block.fields.STOP_OPTION[0]
@@ -1034,14 +1039,17 @@ export class Compiler {
1034
1039
  */
1035
1040
  emitCall(ctx, node, parentId) {
1036
1041
  const segments = node.segments;
1042
+ const entry = this.catalog[node.name];
1037
1043
 
1038
1044
  // List method: `items.push(x)`, `items.at(i)`, ...
1039
1045
  if (segments.length === 2) {
1040
1046
  const listInfo = ctx.resolveList(segments[0]);
1041
- if (listInfo) return this.emitListCall(ctx, node, listInfo, parentId);
1047
+ // Canonical list methods take precedence; aliases resolve in emitListCall
1048
+ // only when the name does not identify a catalog block.
1049
+ if (listInfo && (Object.hasOwn(LIST_OPS, segments[1]) || !entry))
1050
+ return this.emitListCall(ctx, node, listInfo, parentId);
1042
1051
  }
1043
1052
 
1044
- const entry = this.catalog[node.name];
1045
1053
  if (entry) return this.emitCatalogCall(ctx, node, entry, parentId);
1046
1054
 
1047
1055
  if (segments.length === 1 && ctx.funcs.has(node.name)) {
@@ -1098,6 +1106,33 @@ export class Compiler {
1098
1106
  for (const name of inputNames) {
1099
1107
  const spec = entry.inputs[name];
1100
1108
  if (spec.type === "substack") continue;
1109
+ // Variable and list references are stored as fields in Scratch JSON
1110
+ // even though they read as positional arguments in Fx source.
1111
+ if (spec.type === "variable" || spec.type === "list") {
1112
+ if (node.args[argIndex] === undefined)
1113
+ this.fail(
1114
+ `${node.name} requires a ${spec.type} name for ${name}`,
1115
+ node.loc,
1116
+ );
1117
+ const refName = String(this.literalValue(node.args[argIndex]));
1118
+ const resolved =
1119
+ spec.type === "list"
1120
+ ? ctx.resolveList(refName)
1121
+ : ctx.resolveValue(refName);
1122
+ if (
1123
+ !resolved ||
1124
+ (spec.type === "list"
1125
+ ? resolved.kind !== "list"
1126
+ : resolved.kind !== "var")
1127
+ )
1128
+ this.fail(
1129
+ `Unknown ${spec.type} ${JSON.stringify(refName)} for ${node.name}`,
1130
+ node.loc,
1131
+ );
1132
+ fields[name] = [resolved.name, resolved.id];
1133
+ argIndex += 1;
1134
+ continue;
1135
+ }
1101
1136
  if (node.args[argIndex] !== undefined) {
1102
1137
  inputs[name] = this.buildInputForSpec(spec, node.args[argIndex], id);
1103
1138
  argIndex += 1;
package/src/decompiler.js CHANGED
@@ -18,7 +18,15 @@ const MENU_OPCODES = new Set([
18
18
  "looks_backdrops",
19
19
  "sound_sounds_menu",
20
20
  "control_create_clone_of_menu",
21
+ "control_stop_sprite_menu",
21
22
  "sensing_of_object_menu",
23
+ "sensing_fulltouchingobjectmenu",
24
+ "sensing_touchingobjectmenusprites",
25
+ "sensing_scrolldirections",
26
+ "sensing_fingeroptions",
27
+ "looks_changeVisibilityOfSprite_menu",
28
+ "looks_getOtherSpriteVisible_menu",
29
+ "looks_getinput_menu",
22
30
  "pen_menu_colorParam",
23
31
  ]);
24
32
 
@@ -50,6 +58,7 @@ const LIST_OPCODE_METHOD = {
50
58
  };
51
59
 
52
60
  const SAFE_TEMP_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
61
+ const SAFE_VAR_REF = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
53
62
 
54
63
  function escapeString(value) {
55
64
  return String(value)
@@ -236,6 +245,12 @@ class Decompiler {
236
245
  }
237
246
  for (const [inputName, spec] of Object.entries(entry.inputs || {})) {
238
247
  if (spec.type === "substack") continue;
248
+ if (spec.type === "variable" || spec.type === "list") {
249
+ const field = (block.fields || {})[inputName];
250
+ const ref = field ? field[0] : "0";
251
+ args.push(SAFE_VAR_REF.test(ref) ? ref : `"${escapeString(ref)}"`);
252
+ continue;
253
+ }
239
254
  args.push(this.renderExpression(block.inputs[inputName]));
240
255
  }
241
256
  return `${name}(${args.join(", ")})`;
@@ -396,6 +411,18 @@ class Decompiler {
396
411
  option === "timer" ? "timerGreaterThan" : "loudnessGreaterThan";
397
412
  return `${pad}when ${kind}(${this.renderExpression(block.inputs.VALUE)}) {\n${body(block.next)}\n}`;
398
413
  }
414
+ case "event_always":
415
+ return `${pad}when always {\n${body(block.next)}\n}`;
416
+ case "event_whenanything":
417
+ return `${pad}when whenAnything(${this.renderExpression(block.inputs.ANYTHING)}) {\n${body(block.next)}\n}`;
418
+ case "event_whenjavascript":
419
+ return `${pad}when whenJavaScript(${this.renderExpression(block.inputs.JS)}) {\n${body(block.next)}\n}`;
420
+ case "event_whenkeyhit":
421
+ return `${pad}when whenKeyHit("${escapeString((block.fields.KEY_OPTION || ["space"])[0])}") {\n${body(block.next)}\n}`;
422
+ case "event_whenmousescrolled":
423
+ return `${pad}when whenMouseScrolled("${escapeString((block.fields.KEY_OPTION || ["up"])[0])}") {\n${body(block.next)}\n}`;
424
+ case "event_whenstopclicked":
425
+ return `${pad}when whenStopClicked {\n${body(block.next)}\n}`;
399
426
  default:
400
427
  return this.renderExtensionHat(block, pad, body);
401
428
  }