@triflare/fx 0.0.0-stage → 1.0.0

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.
Files changed (39) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +73 -2
  3. package/bin/fx.js +949 -0
  4. package/package.json +64 -4
  5. package/product.yml +28 -0
  6. package/src/brand.js +105 -0
  7. package/src/build.js +181 -0
  8. package/src/catalog.js +753 -0
  9. package/src/compiler.js +1658 -0
  10. package/src/config-edit.js +338 -0
  11. package/src/config.js +400 -0
  12. package/src/decompiler.js +504 -0
  13. package/src/dev.js +390 -0
  14. package/src/errors.js +56 -0
  15. package/src/extensions.js +707 -0
  16. package/src/index.js +31 -0
  17. package/src/language.js +12 -0
  18. package/src/lexer.js +202 -0
  19. package/src/log.js +70 -0
  20. package/src/package.js +202 -0
  21. package/src/packager.js +177 -0
  22. package/src/parser.js +608 -0
  23. package/src/product.js +27 -0
  24. package/src/project.js +156 -0
  25. package/src/sb3.js +340 -0
  26. package/src/sprite.js +81 -0
  27. package/vendor/pm-packager-dist/.fx-build.json +6 -0
  28. package/vendor/pm-packager-dist/LICENSE +202 -0
  29. package/vendor/pm-packager-dist/NOTICE +6 -0
  30. package/vendor/pm-packager-dist/dist/290e09e569a1cab8e61ba93b0d23863f.png +0 -0
  31. package/vendor/pm-packager-dist/dist/icns.js +1 -0
  32. package/vendor/pm-packager-dist/dist/packager.js +154 -0
  33. package/vendor/pm-packager-dist/dist/scaffolding/addons.js +2 -0
  34. package/vendor/pm-packager-dist/dist/scaffolding/extension worker.js +2 -0
  35. package/vendor/pm-packager-dist/dist/scaffolding/scaffolding-full.js +93 -0
  36. package/vendor/pm-packager-dist/dist/scaffolding/scaffolding-min.js +93 -0
  37. package/vendor/pm-packager-dist/dist/scaffolding/vendors~iframe-extension-worker.js +2 -0
  38. package/vendor/pm-packager-dist/dist/vendors~sha256.js +3 -0
  39. package/vendor/pm-packager-dist/package.json +3 -0
package/src/catalog.js ADDED
@@ -0,0 +1,753 @@
1
+ /**
2
+ * Triflare Fx block catalog.
3
+ *
4
+ * This module is the single source of truth for how a Triflare Fx
5
+ * `category.function(...)` call maps onto a Scratch/PenguinMod opcode: its shape
6
+ * (command/reporter/boolean/hat), its inputs, its dropdown fields and any
7
+ * mutation it requires. The compiler, the decompiler and the editor tooling all
8
+ * read from here instead of hard-coding opcode tables.
9
+ *
10
+ * Input descriptors:
11
+ * {type: 'number'|'positive'|'whole'|'integer'|'angle'|'string'|'color'
12
+ * |'boolean'|'substack'|'broadcast'|'variable'|'list'}
13
+ * {type: 'menu', menu: <shadow opcode>, field: <field name>, default: <value>}
14
+ *
15
+ * Field descriptors are plain objects providing the default dropdown value.
16
+ */
17
+
18
+ // Common reusable menu descriptors -----------------------------------------
19
+ const GOTO_MENU = {
20
+ type: "menu",
21
+ menu: "motion_goto_menu",
22
+ field: "TO",
23
+ default: "_mouse_",
24
+ };
25
+ // `motion_glideto` has its own shadow menu block (`motion_glideto_menu`), even
26
+ // though it uses the same `TO` field name as `motion_goto_menu`.
27
+ const GLIDE_MENU = {
28
+ type: "menu",
29
+ menu: "motion_glideto_menu",
30
+ field: "TO",
31
+ default: "_mouse_",
32
+ };
33
+ const POINT_MENU = {
34
+ type: "menu",
35
+ menu: "motion_pointtowards_menu",
36
+ field: "TOWARDS",
37
+ default: "_mouse_",
38
+ };
39
+ const TOUCHING_MENU = {
40
+ type: "menu",
41
+ menu: "sensing_touchingobjectmenu",
42
+ field: "TOUCHINGOBJECTMENU",
43
+ default: "_mouse_",
44
+ };
45
+ const DISTANCE_MENU = {
46
+ type: "menu",
47
+ menu: "sensing_distancetomenu",
48
+ field: "DISTANCETOMENU",
49
+ default: "_mouse_",
50
+ };
51
+ const KEY_MENU = {
52
+ type: "menu",
53
+ menu: "sensing_keyoptions",
54
+ field: "KEY_OPTION",
55
+ default: "space",
56
+ };
57
+ const COSTUME_MENU = {
58
+ type: "menu",
59
+ menu: "looks_costume",
60
+ field: "COSTUME",
61
+ default: "costume1",
62
+ };
63
+ const BACKDROP_MENU = {
64
+ type: "menu",
65
+ menu: "looks_backdrops",
66
+ field: "BACKDROP",
67
+ default: "backdrop1",
68
+ };
69
+ const SOUND_MENU = {
70
+ type: "menu",
71
+ menu: "sound_sounds_menu",
72
+ field: "SOUND_MENU",
73
+ default: "pop",
74
+ };
75
+ const CLONE_MENU = {
76
+ type: "menu",
77
+ menu: "control_create_clone_of_menu",
78
+ field: "CLONE_OPTION",
79
+ default: "_myself_",
80
+ };
81
+ const OF_MENU = {
82
+ type: "menu",
83
+ menu: "sensing_of_object_menu",
84
+ field: "OBJECT",
85
+ default: "_stage_",
86
+ };
87
+
88
+ /** @type {Record<string, object>} */
89
+ export const CATALOG = {
90
+ // ---------------------------------------------------------------- motion
91
+ "motion.moveSteps": {
92
+ opcode: "motion_movesteps",
93
+ kind: "command",
94
+ inputs: { STEPS: { type: "number" } },
95
+ },
96
+ "motion.turnRight": {
97
+ opcode: "motion_turnright",
98
+ kind: "command",
99
+ inputs: { DEGREES: { type: "number" } },
100
+ },
101
+ "motion.turnLeft": {
102
+ opcode: "motion_turnleft",
103
+ kind: "command",
104
+ inputs: { DEGREES: { type: "number" } },
105
+ },
106
+ "motion.goToXY": {
107
+ opcode: "motion_gotoxy",
108
+ kind: "command",
109
+ inputs: { X: { type: "number" }, Y: { type: "number" } },
110
+ },
111
+ "motion.goTo": {
112
+ opcode: "motion_goto",
113
+ kind: "command",
114
+ inputs: { TO: GOTO_MENU },
115
+ },
116
+ "motion.glideToXY": {
117
+ opcode: "motion_glidesecstoxy",
118
+ kind: "command",
119
+ inputs: {
120
+ SECS: { type: "number" },
121
+ X: { type: "number" },
122
+ Y: { type: "number" },
123
+ },
124
+ },
125
+ "motion.glideTo": {
126
+ opcode: "motion_glideto",
127
+ kind: "command",
128
+ inputs: { SECS: { type: "number" }, TO: GLIDE_MENU },
129
+ },
130
+ "motion.pointInDirection": {
131
+ opcode: "motion_pointindirection",
132
+ kind: "command",
133
+ inputs: { DIRECTION: { type: "angle" } },
134
+ },
135
+ "motion.pointTowards": {
136
+ opcode: "motion_pointtowards",
137
+ kind: "command",
138
+ inputs: { TOWARDS: POINT_MENU },
139
+ },
140
+ "motion.changeX": {
141
+ opcode: "motion_changexby",
142
+ kind: "command",
143
+ inputs: { DX: { type: "number" } },
144
+ },
145
+ "motion.setX": {
146
+ opcode: "motion_setx",
147
+ kind: "command",
148
+ inputs: { X: { type: "number" } },
149
+ },
150
+ "motion.changeY": {
151
+ opcode: "motion_changeyby",
152
+ kind: "command",
153
+ inputs: { DY: { type: "number" } },
154
+ },
155
+ "motion.setY": {
156
+ opcode: "motion_sety",
157
+ kind: "command",
158
+ inputs: { Y: { type: "number" } },
159
+ },
160
+ "motion.ifOnEdgeBounce": { opcode: "motion_ifonedgebounce", kind: "command" },
161
+ "motion.setRotationStyle": {
162
+ opcode: "motion_setrotationstyle",
163
+ kind: "command",
164
+ fields: { STYLE: "all around" },
165
+ },
166
+ "motion.xPosition": { opcode: "motion_xposition", kind: "reporter" },
167
+ "motion.yPosition": { opcode: "motion_yposition", kind: "reporter" },
168
+ "motion.direction": { opcode: "motion_direction", kind: "reporter" },
169
+
170
+ // ----------------------------------------------------------------- looks
171
+ "looks.say": {
172
+ opcode: "looks_say",
173
+ kind: "command",
174
+ inputs: { MESSAGE: { type: "string" } },
175
+ },
176
+ "looks.sayForSecs": {
177
+ opcode: "looks_sayforsecs",
178
+ kind: "command",
179
+ inputs: { MESSAGE: { type: "string" }, SECS: { type: "number" } },
180
+ },
181
+ "looks.think": {
182
+ opcode: "looks_think",
183
+ kind: "command",
184
+ inputs: { MESSAGE: { type: "string" } },
185
+ },
186
+ "looks.thinkForSecs": {
187
+ opcode: "looks_thinkforsecs",
188
+ kind: "command",
189
+ inputs: { MESSAGE: { type: "string" }, SECS: { type: "number" } },
190
+ },
191
+ "looks.switchCostume": {
192
+ opcode: "looks_switchcostumeto",
193
+ kind: "command",
194
+ inputs: { COSTUME: COSTUME_MENU },
195
+ },
196
+ "looks.nextCostume": { opcode: "looks_nextcostume", kind: "command" },
197
+ "looks.switchBackdrop": {
198
+ opcode: "looks_switchbackdropto",
199
+ kind: "command",
200
+ inputs: { BACKDROP: BACKDROP_MENU },
201
+ },
202
+ "looks.switchBackdropAndWait": {
203
+ opcode: "looks_switchbackdroptoandwait",
204
+ kind: "command",
205
+ inputs: { BACKDROP: BACKDROP_MENU },
206
+ },
207
+ "looks.nextBackdrop": { opcode: "looks_nextbackdrop", kind: "command" },
208
+ "looks.changeSize": {
209
+ opcode: "looks_changesizeby",
210
+ kind: "command",
211
+ inputs: { CHANGE: { type: "number" } },
212
+ },
213
+ "looks.setSize": {
214
+ opcode: "looks_setsizeto",
215
+ kind: "command",
216
+ inputs: { SIZE: { type: "number" } },
217
+ },
218
+ "looks.changeEffect": {
219
+ opcode: "looks_changeeffectby",
220
+ kind: "command",
221
+ fields: { EFFECT: "color" },
222
+ inputs: { CHANGE: { type: "number" } },
223
+ },
224
+ "looks.setEffect": {
225
+ opcode: "looks_seteffectto",
226
+ kind: "command",
227
+ fields: { EFFECT: "color" },
228
+ inputs: { VALUE: { type: "number" } },
229
+ },
230
+ "looks.clearEffects": {
231
+ opcode: "looks_cleargraphiceffects",
232
+ kind: "command",
233
+ },
234
+ "looks.show": { opcode: "looks_show", kind: "command" },
235
+ "looks.hide": { opcode: "looks_hide", kind: "command" },
236
+ "looks.goToFrontLayer": {
237
+ opcode: "looks_gotofrontback",
238
+ kind: "command",
239
+ fields: { FRONT_BACK: "front" },
240
+ },
241
+ "looks.goLayers": {
242
+ opcode: "looks_goforwardbackwardlayers",
243
+ kind: "command",
244
+ fields: { FORWARD_BACKWARD: "forward" },
245
+ inputs: { NUM: { type: "integer" } },
246
+ },
247
+ "looks.costumeNumber": {
248
+ opcode: "looks_costumenumbername",
249
+ kind: "reporter",
250
+ fields: { NUMBER_NAME: "number" },
251
+ },
252
+ "looks.backdropNumber": {
253
+ opcode: "looks_backdropnumbername",
254
+ kind: "reporter",
255
+ fields: { NUMBER_NAME: "number" },
256
+ },
257
+ "looks.size": { opcode: "looks_size", kind: "reporter" },
258
+
259
+ // ----------------------------------------------------------------- sound
260
+ "sound.play": {
261
+ opcode: "sound_play",
262
+ kind: "command",
263
+ inputs: { SOUND_MENU: SOUND_MENU },
264
+ },
265
+ "sound.playUntilDone": {
266
+ opcode: "sound_playuntildone",
267
+ kind: "command",
268
+ inputs: { SOUND_MENU: SOUND_MENU },
269
+ },
270
+ "sound.stopAllSounds": { opcode: "sound_stopallsounds", kind: "command" },
271
+ "sound.changeEffect": {
272
+ opcode: "sound_changeeffectby",
273
+ kind: "command",
274
+ fields: { EFFECT: "pitch" },
275
+ inputs: { VALUE: { type: "number" } },
276
+ },
277
+ "sound.setEffect": {
278
+ opcode: "sound_seteffectto",
279
+ kind: "command",
280
+ fields: { EFFECT: "pitch" },
281
+ inputs: { VALUE: { type: "number" } },
282
+ },
283
+ "sound.clearEffects": { opcode: "sound_cleareffects", kind: "command" },
284
+ "sound.changeVolume": {
285
+ opcode: "sound_changevolumeby",
286
+ kind: "command",
287
+ inputs: { VOLUME: { type: "number" } },
288
+ },
289
+ "sound.setVolume": {
290
+ opcode: "sound_setvolumeto",
291
+ kind: "command",
292
+ inputs: { VOLUME: { type: "number" } },
293
+ },
294
+ "sound.volume": { opcode: "sound_volume", kind: "reporter" },
295
+
296
+ // --------------------------------------------------------------- sensing
297
+ "sensing.touching": {
298
+ opcode: "sensing_touchingobject",
299
+ kind: "boolean",
300
+ inputs: { TOUCHINGOBJECTMENU: TOUCHING_MENU },
301
+ },
302
+ "sensing.touchingColor": {
303
+ opcode: "sensing_touchingcolor",
304
+ kind: "boolean",
305
+ inputs: { COLOR: { type: "color" } },
306
+ },
307
+ "sensing.colorIsTouchingColor": {
308
+ opcode: "sensing_coloristouchingcolor",
309
+ kind: "boolean",
310
+ inputs: { COLOR: { type: "color" }, COLOR2: { type: "color" } },
311
+ },
312
+ "sensing.distanceTo": {
313
+ opcode: "sensing_distanceto",
314
+ kind: "reporter",
315
+ inputs: { DISTANCETOMENU: DISTANCE_MENU },
316
+ },
317
+ "sensing.askAndWait": {
318
+ opcode: "sensing_askandwait",
319
+ kind: "command",
320
+ inputs: { QUESTION: { type: "string" } },
321
+ },
322
+ "sensing.answer": { opcode: "sensing_answer", kind: "reporter" },
323
+ "sensing.keyPressed": {
324
+ opcode: "sensing_keypressed",
325
+ kind: "boolean",
326
+ inputs: { KEY_OPTION: KEY_MENU },
327
+ },
328
+ "sensing.mouseDown": { opcode: "sensing_mousedown", kind: "boolean" },
329
+ "sensing.mouseX": { opcode: "sensing_mousex", kind: "reporter" },
330
+ "sensing.mouseY": { opcode: "sensing_mousey", kind: "reporter" },
331
+ "sensing.loudness": { opcode: "sensing_loudness", kind: "reporter" },
332
+ "sensing.timer": { opcode: "sensing_timer", kind: "reporter" },
333
+ "sensing.resetTimer": { opcode: "sensing_resettimer", kind: "command" },
334
+ "sensing.propertyOf": {
335
+ opcode: "sensing_of",
336
+ kind: "reporter",
337
+ fields: { PROPERTY: "x position" },
338
+ inputs: { OBJECT: OF_MENU },
339
+ },
340
+ "sensing.current": {
341
+ opcode: "sensing_current",
342
+ kind: "reporter",
343
+ fields: { CURRENTMENU: "year" },
344
+ },
345
+ "sensing.daysSince2000": {
346
+ opcode: "sensing_dayssince2000",
347
+ kind: "reporter",
348
+ },
349
+ "sensing.username": { opcode: "sensing_username", kind: "reporter" },
350
+
351
+ // ------------------------------------------------------------- operators
352
+ "operators.add": {
353
+ opcode: "operator_add",
354
+ kind: "reporter",
355
+ inputs: { NUM1: { type: "number" }, NUM2: { type: "number" } },
356
+ },
357
+ "operators.subtract": {
358
+ opcode: "operator_subtract",
359
+ kind: "reporter",
360
+ inputs: { NUM1: { type: "number" }, NUM2: { type: "number" } },
361
+ },
362
+ "operators.multiply": {
363
+ opcode: "operator_multiply",
364
+ kind: "reporter",
365
+ inputs: { NUM1: { type: "number" }, NUM2: { type: "number" } },
366
+ },
367
+ "operators.divide": {
368
+ opcode: "operator_divide",
369
+ kind: "reporter",
370
+ inputs: { NUM1: { type: "number" }, NUM2: { type: "number" } },
371
+ },
372
+ "operators.random": {
373
+ opcode: "operator_random",
374
+ kind: "reporter",
375
+ inputs: { FROM: { type: "number" }, TO: { type: "number" } },
376
+ },
377
+ "operators.gt": {
378
+ opcode: "operator_gt",
379
+ kind: "boolean",
380
+ inputs: { OPERAND1: { type: "string" }, OPERAND2: { type: "string" } },
381
+ },
382
+ "operators.lt": {
383
+ opcode: "operator_lt",
384
+ kind: "boolean",
385
+ inputs: { OPERAND1: { type: "string" }, OPERAND2: { type: "string" } },
386
+ },
387
+ "operators.equals": {
388
+ opcode: "operator_equals",
389
+ kind: "boolean",
390
+ inputs: { OPERAND1: { type: "string" }, OPERAND2: { type: "string" } },
391
+ },
392
+ "operators.and": {
393
+ opcode: "operator_and",
394
+ kind: "boolean",
395
+ inputs: { OPERAND1: { type: "boolean" }, OPERAND2: { type: "boolean" } },
396
+ },
397
+ "operators.or": {
398
+ opcode: "operator_or",
399
+ kind: "boolean",
400
+ inputs: { OPERAND1: { type: "boolean" }, OPERAND2: { type: "boolean" } },
401
+ },
402
+ "operators.not": {
403
+ opcode: "operator_not",
404
+ kind: "boolean",
405
+ inputs: { OPERAND: { type: "boolean" } },
406
+ },
407
+ "operators.join": {
408
+ opcode: "operator_join",
409
+ kind: "reporter",
410
+ inputs: { STRING1: { type: "string" }, STRING2: { type: "string" } },
411
+ },
412
+ "operators.letterOf": {
413
+ opcode: "operator_letter_of",
414
+ kind: "reporter",
415
+ inputs: { LETTER: { type: "number" }, STRING: { type: "string" } },
416
+ },
417
+ "operators.length": {
418
+ opcode: "operator_length",
419
+ kind: "reporter",
420
+ inputs: { STRING: { type: "string" } },
421
+ },
422
+ "operators.contains": {
423
+ opcode: "operator_contains",
424
+ kind: "boolean",
425
+ inputs: { STRING1: { type: "string" }, STRING2: { type: "string" } },
426
+ },
427
+ "operators.mod": {
428
+ opcode: "operator_mod",
429
+ kind: "reporter",
430
+ inputs: { NUM1: { type: "number" }, NUM2: { type: "number" } },
431
+ },
432
+ "operators.round": {
433
+ opcode: "operator_round",
434
+ kind: "reporter",
435
+ inputs: { NUM: { type: "number" } },
436
+ },
437
+ "operators.math": {
438
+ opcode: "operator_mathop",
439
+ kind: "reporter",
440
+ fields: { OPERATOR: "abs" },
441
+ inputs: { NUM: { type: "number" } },
442
+ },
443
+
444
+ // --------------------------------------------------------------- control
445
+ "control.wait": {
446
+ opcode: "control_wait",
447
+ kind: "command",
448
+ inputs: { DURATION: { type: "positive" } },
449
+ },
450
+ "control.repeat": {
451
+ opcode: "control_repeat",
452
+ kind: "command",
453
+ inputs: { TIMES: { type: "whole" }, SUBSTACK: { type: "substack" } },
454
+ },
455
+ "control.forever": {
456
+ opcode: "control_forever",
457
+ kind: "command",
458
+ inputs: { SUBSTACK: { type: "substack" } },
459
+ // PenguinMod builds this loop without a next-statement connection and only
460
+ // creates one from the block's mutation (`hasbreak` — the nub the editor
461
+ // adds by itself once a block sits below the loop). Blocks stacked under a
462
+ // `control.forever` therefore need that mutation: without it the editor's
463
+ // XML loader dereferences the missing connection while reading the
464
+ // following `<next>` element and the whole workspace update fails.
465
+ dynamicNext: true,
466
+ },
467
+ "control.if": {
468
+ opcode: "control_if",
469
+ kind: "command",
470
+ inputs: { CONDITION: { type: "boolean" }, SUBSTACK: { type: "substack" } },
471
+ },
472
+ "control.ifElse": {
473
+ opcode: "control_if_else",
474
+ kind: "command",
475
+ inputs: {
476
+ CONDITION: { type: "boolean" },
477
+ SUBSTACK: { type: "substack" },
478
+ SUBSTACK2: { type: "substack" },
479
+ },
480
+ },
481
+ "control.waitUntil": {
482
+ opcode: "control_wait_until",
483
+ kind: "command",
484
+ inputs: { CONDITION: { type: "boolean" } },
485
+ },
486
+ "control.repeatUntil": {
487
+ opcode: "control_repeat_until",
488
+ kind: "command",
489
+ inputs: { CONDITION: { type: "boolean" }, SUBSTACK: { type: "substack" } },
490
+ },
491
+ "control.stop": {
492
+ opcode: "control_stop",
493
+ kind: "command",
494
+ fields: { STOP_OPTION: "all" },
495
+ stopBlock: true,
496
+ },
497
+ "control.clone": {
498
+ opcode: "control_create_clone_of",
499
+ kind: "command",
500
+ inputs: { CLONE_OPTION: CLONE_MENU },
501
+ },
502
+ "control.deleteThisClone": {
503
+ opcode: "control_delete_this_clone",
504
+ kind: "command",
505
+ },
506
+
507
+ // ---------------------------------------------------------------- events
508
+ "events.broadcast": {
509
+ opcode: "event_broadcast",
510
+ kind: "command",
511
+ inputs: { BROADCAST_INPUT: { type: "broadcast" } },
512
+ },
513
+ "events.broadcastAndWait": {
514
+ opcode: "event_broadcastandwait",
515
+ kind: "command",
516
+ inputs: { BROADCAST_INPUT: { type: "broadcast" } },
517
+ },
518
+
519
+ // --------------------------------------------------------------- hats
520
+ "events.greenFlag": {
521
+ opcode: "event_whenflagclicked",
522
+ kind: "hat",
523
+ args: [],
524
+ },
525
+ "events.keyPressed": {
526
+ opcode: "event_whenkeypressed",
527
+ kind: "hat",
528
+ args: [{ field: "KEY_OPTION", default: "space" }],
529
+ },
530
+ "events.spriteClicked": {
531
+ opcode: "event_whenthisspriteclicked",
532
+ kind: "hat",
533
+ args: [],
534
+ },
535
+ "events.stageClicked": {
536
+ opcode: "event_whenstageclicked",
537
+ kind: "hat",
538
+ args: [],
539
+ },
540
+ "events.cloneStart": {
541
+ opcode: "control_start_as_clone",
542
+ kind: "hat",
543
+ args: [],
544
+ },
545
+ "events.broadcastReceived": {
546
+ opcode: "event_whenbroadcastreceived",
547
+ kind: "hat",
548
+ args: [{ field: "BROADCAST_OPTION", type: "broadcast" }],
549
+ },
550
+ "events.backdropSwitchesTo": {
551
+ opcode: "event_whenbackdropswitchesto",
552
+ kind: "hat",
553
+ args: [{ field: "BACKDROP", default: "backdrop1" }],
554
+ },
555
+ // The dropdown field is `WHENGREATERTHANMENU` in TurboWarp/PenguinMod (the
556
+ // vanilla Scratch 3 name is `WHENGREATER`); the option values are the
557
+ // uppercase menu ids `LOUDNESS`/`TIMER`. See PenguinMod-Blocks event.js and
558
+ // scratch3_event.js.
559
+ "events.loudnessGreaterThan": {
560
+ opcode: "event_whengreaterthan",
561
+ kind: "hat",
562
+ args: [
563
+ { input: "VALUE", type: "number" },
564
+ { field: "WHENGREATERTHANMENU", value: "LOUDNESS" },
565
+ ],
566
+ },
567
+ "events.timerGreaterThan": {
568
+ opcode: "event_whengreaterthan",
569
+ kind: "hat",
570
+ args: [
571
+ { input: "VALUE", type: "number" },
572
+ { field: "WHENGREATERTHANMENU", value: "TIMER" },
573
+ ],
574
+ },
575
+
576
+ // ------------------------------------------------------------------ pen
577
+ "pen.clear": { opcode: "pen_clear", kind: "command", extension: "pen" },
578
+ "pen.stamp": { opcode: "pen_stamp", kind: "command", extension: "pen" },
579
+ "pen.penDown": { opcode: "pen_penDown", kind: "command", extension: "pen" },
580
+ "pen.penUp": { opcode: "pen_penUp", kind: "command", extension: "pen" },
581
+ "pen.setColor": {
582
+ opcode: "pen_setPenColorToColor",
583
+ kind: "command",
584
+ inputs: { COLOR: { type: "color" } },
585
+ extension: "pen",
586
+ },
587
+ "pen.changeSize": {
588
+ opcode: "pen_changePenSizeBy",
589
+ kind: "command",
590
+ inputs: { SIZE: { type: "number" } },
591
+ extension: "pen",
592
+ },
593
+ "pen.setSize": {
594
+ opcode: "pen_setPenSizeTo",
595
+ kind: "command",
596
+ inputs: { SIZE: { type: "number" } },
597
+ extension: "pen",
598
+ },
599
+ // COLOR_PARAM is an input whose shadow is `pen_menu_colorParam`, not a
600
+ // direct field: the pen extension declares `colorParam` with
601
+ // `acceptReporters: true`, so the workspace serializes it as a value input
602
+ // wrapping a shadow menu block (field name `colorParam`).
603
+ "pen.changeColorParam": {
604
+ opcode: "pen_changePenColorParamBy",
605
+ kind: "command",
606
+ inputs: {
607
+ COLOR_PARAM: {
608
+ type: "menu",
609
+ menu: "pen_menu_colorParam",
610
+ field: "colorParam",
611
+ default: "color",
612
+ },
613
+ VALUE: { type: "number" },
614
+ },
615
+ extension: "pen",
616
+ },
617
+ "pen.setColorParam": {
618
+ opcode: "pen_setPenColorParamTo",
619
+ kind: "command",
620
+ inputs: {
621
+ COLOR_PARAM: {
622
+ type: "menu",
623
+ menu: "pen_menu_colorParam",
624
+ field: "colorParam",
625
+ default: "color",
626
+ },
627
+ VALUE: { type: "number" },
628
+ },
629
+ extension: "pen",
630
+ },
631
+
632
+ // ------------------------------------------------------------ penguinmod
633
+ "pm.lastKeyPressed": {
634
+ opcode: "argument_reporter_string_number",
635
+ kind: "reporter",
636
+ fields: { VALUE: ["last key pressed", null] },
637
+ },
638
+ "pm.isCompiled": {
639
+ opcode: "argument_reporter_boolean",
640
+ kind: "boolean",
641
+ fields: { VALUE: ["is compiled?", null] },
642
+ },
643
+ "pm.isPenguinMod": {
644
+ opcode: "argument_reporter_boolean",
645
+ kind: "boolean",
646
+ fields: { VALUE: ["is penguinmod?", null] },
647
+ },
648
+ };
649
+
650
+ /**
651
+ * Native Scratch variable/list mutation opcodes used when a list method is
652
+ * called (`items.push(x)` etc.).
653
+ */
654
+ export const LIST_OPS = {
655
+ push: {
656
+ opcode: "data_addtolist",
657
+ kind: "command",
658
+ inputs: ["ITEM"],
659
+ aliases: ["add", "append"],
660
+ },
661
+ insert: {
662
+ opcode: "data_insertatlist",
663
+ kind: "command",
664
+ inputs: ["INDEX", "ITEM"],
665
+ },
666
+ replace: {
667
+ opcode: "data_replaceitemoflist",
668
+ kind: "command",
669
+ inputs: ["INDEX", "ITEM"],
670
+ aliases: ["set"],
671
+ },
672
+ deleteAt: {
673
+ opcode: "data_deleteoflist",
674
+ kind: "command",
675
+ inputs: ["INDEX"],
676
+ aliases: ["remove", "delete"],
677
+ },
678
+ deleteAll: {
679
+ opcode: "data_deletealloflist",
680
+ kind: "command",
681
+ inputs: [],
682
+ aliases: ["clear"],
683
+ },
684
+ show: { opcode: "data_showlist", kind: "command", inputs: [] },
685
+ hide: { opcode: "data_hidelist", kind: "command", inputs: [] },
686
+ itemAt: {
687
+ opcode: "data_itemoflist",
688
+ kind: "reporter",
689
+ inputs: ["INDEX"],
690
+ aliases: ["at"],
691
+ },
692
+ itemNumber: {
693
+ opcode: "data_itemnumoflist",
694
+ kind: "reporter",
695
+ inputs: ["ITEM"],
696
+ aliases: ["indexOf"],
697
+ },
698
+ length: { opcode: "data_lengthoflist", kind: "reporter", inputs: [] },
699
+ contains: {
700
+ opcode: "data_listcontainsitem",
701
+ kind: "boolean",
702
+ inputs: ["ITEM"],
703
+ },
704
+ };
705
+
706
+ /** Reverse lookup built once: opcode -> canonical catalog name. */
707
+ export const BY_OPCODE = (() => {
708
+ const map = new Map();
709
+ for (const [name, entry] of Object.entries(CATALOG)) {
710
+ if (!map.has(entry.opcode)) map.set(entry.opcode, name);
711
+ }
712
+ return map;
713
+ })();
714
+
715
+ /** Core (non-extension) opcode prefixes. */
716
+ export const CORE_EXTENSIONS = new Set([
717
+ "argument",
718
+ "colour",
719
+ "control",
720
+ "data",
721
+ "event",
722
+ "looks",
723
+ "math",
724
+ "motion",
725
+ "operator",
726
+ "procedures",
727
+ "sensing",
728
+ "sound",
729
+ ]);
730
+
731
+ /**
732
+ * Determine the extension id (if any) required by an opcode.
733
+ * @param {string} opcode
734
+ * @returns {string|null}
735
+ */
736
+ export function extensionForOpcode(opcode) {
737
+ const index = opcode.indexOf("_");
738
+ if (index < 0) return null;
739
+ const prefix = opcode.slice(0, index);
740
+ if (CORE_EXTENSIONS.has(prefix)) return null;
741
+ return prefix;
742
+ }
743
+
744
+ /** Resolve a catalog entry, following aliases like `motion.moveSteps`. */
745
+ export function lookup(name) {
746
+ if (Object.prototype.hasOwnProperty.call(CATALOG, name)) return CATALOG[name];
747
+ return null;
748
+ }
749
+
750
+ /** List every canonical block name in the catalog. */
751
+ export function blockNames() {
752
+ return Object.keys(CATALOG).sort();
753
+ }