@world-engines/blocks-editor 0.1.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 (76) hide show
  1. package/LICENSE +46 -0
  2. package/README.md +40 -0
  3. package/dist/adapters/trigger.d.ts +72 -0
  4. package/dist/adapters/trigger.js +206 -0
  5. package/dist/blockly-config.d.ts +3 -0
  6. package/dist/blockly-config.js +7 -0
  7. package/dist/blocks/ctx.d.ts +67 -0
  8. package/dist/blocks/ctx.js +96 -0
  9. package/dist/blocks/registry-taxonomy.d.ts +12 -0
  10. package/dist/blocks/registry-taxonomy.js +79 -0
  11. package/dist/blocks/registry.d.ts +39 -0
  12. package/dist/blocks/registry.js +1000 -0
  13. package/dist/blocks/time-fields.d.ts +16 -0
  14. package/dist/blocks/time-fields.js +45 -0
  15. package/dist/blocks/toolbox.d.ts +53 -0
  16. package/dist/blocks/toolbox.js +195 -0
  17. package/dist/fields/AttrPickerField.d.ts +13 -0
  18. package/dist/fields/AttrPickerField.js +48 -0
  19. package/dist/fields/EdgePickerField.d.ts +10 -0
  20. package/dist/fields/EdgePickerField.js +36 -0
  21. package/dist/fields/EntityPickerField.d.ts +10 -0
  22. package/dist/fields/EntityPickerField.js +36 -0
  23. package/dist/fields/StatePickerField.d.ts +13 -0
  24. package/dist/fields/StatePickerField.js +47 -0
  25. package/dist/fields/TimePickerField.d.ts +10 -0
  26. package/dist/fields/TimePickerField.js +81 -0
  27. package/dist/fields/picker-events.d.ts +21 -0
  28. package/dist/fields/picker-events.js +28 -0
  29. package/dist/fields/register.d.ts +6 -0
  30. package/dist/fields/register.js +22 -0
  31. package/dist/i18n/messages-zh.d.ts +6 -0
  32. package/dist/i18n/messages-zh.js +34 -0
  33. package/dist/index.d.ts +21 -0
  34. package/dist/index.js +11 -0
  35. package/dist/migration/index.d.ts +1 -0
  36. package/dist/migration/index.js +1 -0
  37. package/dist/migration/types.d.ts +91 -0
  38. package/dist/migration/types.js +1 -0
  39. package/dist/react/PickerHost.d.ts +6 -0
  40. package/dist/react/PickerHost.js +110 -0
  41. package/dist/sdk-to-block/index.d.ts +4 -0
  42. package/dist/sdk-to-block/index.js +4 -0
  43. package/dist/sdk-to-block/parser.d.ts +2 -0
  44. package/dist/sdk-to-block/parser.js +101 -0
  45. package/dist/sdk-to-block/register.d.ts +3 -0
  46. package/dist/sdk-to-block/register.js +27 -0
  47. package/dist/sdk-to-block/signature.d.ts +31 -0
  48. package/dist/sdk-to-block/signature.js +12 -0
  49. package/dist/sdk-to-block/to-block-def.d.ts +25 -0
  50. package/dist/sdk-to-block/to-block-def.js +71 -0
  51. package/dist/themes/index.d.ts +4 -0
  52. package/dist/themes/index.js +12 -0
  53. package/dist/themes/liquid-glass-dark.d.ts +57 -0
  54. package/dist/themes/liquid-glass-dark.js +187 -0
  55. package/dist/themes/liquid-glass.css +195 -0
  56. package/dist/view-to-block/index.d.ts +10 -0
  57. package/dist/view-to-block/index.js +67 -0
  58. package/dist/workspace.d.ts +33 -0
  59. package/dist/workspace.js +306 -0
  60. package/package.json +65 -0
  61. package/public/blockly-media/1x1.gif +0 -0
  62. package/public/blockly-media/click.mp3 +0 -0
  63. package/public/blockly-media/delete-icon.svg +1 -0
  64. package/public/blockly-media/delete.mp3 +0 -0
  65. package/public/blockly-media/disconnect.mp3 +0 -0
  66. package/public/blockly-media/drop.mp3 +0 -0
  67. package/public/blockly-media/dropdown-arrow.svg +1 -0
  68. package/public/blockly-media/foldout-icon.svg +1 -0
  69. package/public/blockly-media/handclosed.cur +0 -0
  70. package/public/blockly-media/handdelete.cur +0 -0
  71. package/public/blockly-media/handopen.cur +0 -0
  72. package/public/blockly-media/pilcrow.png +0 -0
  73. package/public/blockly-media/quote0.png +0 -0
  74. package/public/blockly-media/quote1.png +0 -0
  75. package/public/blockly-media/resize-handle.svg +3 -0
  76. package/public/blockly-media/sprites.svg +74 -0
@@ -0,0 +1,1000 @@
1
+ import * as Blockly from "blockly";
2
+ import { registerFieldMultilineInput } from "@blockly/field-multilineinput";
3
+ import { DEFAULT_WORLD_TIME_BOUNDS, TIME_FIELDS } from "./time-fields.js";
4
+ import { attrOptionsForEntity, entityOptions, eventOptions, PLACEHOLDER_ATTR_OPTION, readCtxFromWorkspace } from "./ctx.js";
5
+ import { BLOCK_STYLE_KEYS } from "../themes/liquid-glass-dark.js";
6
+ import { FIELD_ATTR_PICKER, FIELD_ENTITY_PICKER, registerPickerFieldsOnce } from "../fields/register.js";
7
+ // 保留旧 8 block 类型 兼容旧 IDB 数据
8
+ export const BLOCK_TYPES = {
9
+ triggerRoot: "trigger_def_root",
10
+ // 触发器定义树沿用 statement 连接;Scratch 运算区的 op_and/op_or/op_not
11
+ // 是 Boolean value 连接。两者不得复用类型名,否则后注册的 reporter 定义
12
+ // 会覆盖触发器定义,导致旧触发器工作区反序列化时连接类型崩溃。
13
+ opAnd: "trigger_def_and",
14
+ opOr: "trigger_def_or",
15
+ opNot: "trigger_def_not",
16
+ leafTime: "trigger_def_time",
17
+ leafEvent: "trigger_def_event",
18
+ leafNl: "trigger_def_nl",
19
+ leafAttrRef: "trigger_def_attr_ref",
20
+ leafRelationRef: "trigger_def_relation_ref",
21
+ leafDaySegment: "trigger_def_day_segment"
22
+ };
23
+ export const FIELD_NAMES = {
24
+ closeMode: "close_mode",
25
+ eventId: "event_id",
26
+ delayMs: "delay_ms",
27
+ delaySeconds: "delay_seconds",
28
+ text: "text",
29
+ milestone: "milestone",
30
+ entityId: "entity_id",
31
+ attrKey: "attr_key",
32
+ srcId: "src_id",
33
+ dstId: "dst_id",
34
+ kind: "kind",
35
+ segment: "segment",
36
+ expectedValue: "expected_value"
37
+ };
38
+ export const INPUT_NAMES = {
39
+ tree: "tree",
40
+ closeWhen: "close_when",
41
+ children: "children",
42
+ child: "child"
43
+ };
44
+ export const CLOSE_MODE_OPTIONS = [
45
+ ["永久关闭", "permanent"],
46
+ ["临时关闭", "temporary"]
47
+ ];
48
+ const FIELD_MULTILINE_TEXT = "field_multilinetext";
49
+ const MUTATOR_OP_CHILDREN = "op_children_mutator";
50
+ const EXT_LEAF_EVENT_DROPDOWN = "leaf_event_dropdown_ext";
51
+ const EXT_LEAF_ATTR_REF_DROPDOWN = "leaf_attr_ref_dropdown_ext";
52
+ const EXT_LEAF_TIME_INIT = "leaf_time_init_ext";
53
+ const DEFAULT_CHILD_COUNT = 2;
54
+ const MIN_CHILD_COUNT = 2;
55
+ const MAX_CHILD_COUNT = 16;
56
+ const LEAF_NL_DEFAULT_MAX_LINES = 4;
57
+ const TIME_FIELD_LABELS = {
58
+ year: "年",
59
+ month: "月",
60
+ day: "日",
61
+ hour: "时",
62
+ minute: "分",
63
+ second: "秒"
64
+ };
65
+ const opChildrenMutator = {
66
+ child_count_: DEFAULT_CHILD_COUNT,
67
+ saveExtraState() {
68
+ return { child_count: this.child_count_ };
69
+ },
70
+ loadExtraState(state) {
71
+ const next = Math.max(MIN_CHILD_COUNT, Math.min(MAX_CHILD_COUNT, Math.floor(state.child_count)));
72
+ this.child_count_ = next;
73
+ this.updateShape_();
74
+ },
75
+ updateShape_() {
76
+ let i = 0;
77
+ while (this.getInput(`${INPUT_NAMES.children}_${i}`)) {
78
+ this.removeInput(`${INPUT_NAMES.children}_${i}`);
79
+ i++;
80
+ }
81
+ for (let idx = 0; idx < this.child_count_; idx++) {
82
+ this.appendStatementInput(`${INPUT_NAMES.children}_${idx}`).setCheck(null);
83
+ }
84
+ }
85
+ };
86
+ const dropdownGeneratorForEvent = function () {
87
+ const ctx = readCtxFromWorkspace(this.getSourceBlock()?.workspace ?? null);
88
+ return eventOptions(ctx).map((o) => [o[0], o[1]]);
89
+ };
90
+ const dropdownGeneratorForEntity = function () {
91
+ const ctx = readCtxFromWorkspace(this.getSourceBlock()?.workspace ?? null);
92
+ return entityOptions(ctx).map((o) => [o[0], o[1]]);
93
+ };
94
+ const attrDropdownGenerator = function () {
95
+ const source = this.getSourceBlock();
96
+ const ctx = readCtxFromWorkspace(source?.workspace ?? null);
97
+ if (source && typeof source === "object" && "getFieldValue" in source) {
98
+ const entityId = source.getFieldValue(FIELD_NAMES.entityId);
99
+ if (typeof entityId === "string") {
100
+ return attrOptionsForEntity(ctx, entityId).map((o) => [o[0], o[1]]);
101
+ }
102
+ }
103
+ return [[PLACEHOLDER_ATTR_OPTION[0], PLACEHOLDER_ATTR_OPTION[1]]];
104
+ };
105
+ function leafEventInit() {
106
+ this.appendDummyInput()
107
+ .appendField("事件")
108
+ .appendField(new Blockly.FieldDropdown(dropdownGeneratorForEvent), FIELD_NAMES.eventId);
109
+ this.appendDummyInput()
110
+ .appendField("延迟(剧情秒)")
111
+ .appendField(new Blockly.FieldNumber(0, 0), FIELD_NAMES.delaySeconds);
112
+ this.setPreviousStatement(true);
113
+ this.setNextStatement(true);
114
+ this.setStyle(BLOCK_STYLE_KEYS.dataEvent);
115
+ }
116
+ function leafTimeInit() {
117
+ const ctx = readCtxFromWorkspace(this.workspace);
118
+ const bounds = ctx.worldTime ?? DEFAULT_WORLD_TIME_BOUNDS;
119
+ const input = this.appendDummyInput();
120
+ input.appendField("世界时间到达");
121
+ for (const spec of TIME_FIELDS) {
122
+ const lo = spec.lo(bounds);
123
+ const hi = spec.hi(bounds);
124
+ input.appendField(TIME_FIELD_LABELS[spec.key] ?? spec.key);
125
+ input.appendField(new Blockly.FieldNumber(lo, lo, hi), spec.key);
126
+ }
127
+ this.setInputsInline(true);
128
+ this.setPreviousStatement(true);
129
+ this.setNextStatement(true);
130
+ this.setStyle(BLOCK_STYLE_KEYS.time);
131
+ }
132
+ function leafAttrRefInit() {
133
+ this.appendDummyInput()
134
+ .appendField("实体")
135
+ .appendField(new Blockly.FieldDropdown(dropdownGeneratorForEntity), FIELD_NAMES.entityId);
136
+ this.appendDummyInput()
137
+ .appendField("属性")
138
+ .appendField(new Blockly.FieldDropdown(attrDropdownGenerator), FIELD_NAMES.attrKey);
139
+ this.appendDummyInput()
140
+ .appendField("期望值")
141
+ .appendField(new Blockly.FieldTextInput(""), FIELD_NAMES.expectedValue);
142
+ this.appendDummyInput()
143
+ .appendField("延迟(剧情秒)")
144
+ .appendField(new Blockly.FieldNumber(0, 0), FIELD_NAMES.delaySeconds);
145
+ this.setPreviousStatement(true);
146
+ this.setNextStatement(true);
147
+ this.setStyle(BLOCK_STYLE_KEYS.dataAttr);
148
+ }
149
+ let extensionsRegistered = false;
150
+ function registerExtensionsOnce() {
151
+ if (extensionsRegistered)
152
+ return;
153
+ if (!Blockly.Extensions.isRegistered(MUTATOR_OP_CHILDREN)) {
154
+ Blockly.Extensions.registerMutator(MUTATOR_OP_CHILDREN, opChildrenMutator);
155
+ }
156
+ if (!Blockly.Extensions.isRegistered(EXT_LEAF_EVENT_DROPDOWN)) {
157
+ Blockly.Extensions.register(EXT_LEAF_EVENT_DROPDOWN, leafEventInit);
158
+ }
159
+ if (!Blockly.Extensions.isRegistered(EXT_LEAF_ATTR_REF_DROPDOWN)) {
160
+ Blockly.Extensions.register(EXT_LEAF_ATTR_REF_DROPDOWN, leafAttrRefInit);
161
+ }
162
+ if (!Blockly.Extensions.isRegistered(EXT_LEAF_TIME_INIT)) {
163
+ Blockly.Extensions.register(EXT_LEAF_TIME_INIT, leafTimeInit);
164
+ }
165
+ extensionsRegistered = true;
166
+ }
167
+ let multilineFieldRegistered = false;
168
+ function registerMultilineFieldOnce() {
169
+ if (multilineFieldRegistered)
170
+ return;
171
+ registerFieldMultilineInput();
172
+ multilineFieldRegistered = true;
173
+ }
174
+ // 旧 8 block 定义 (兼容 IDB v1 数据 toolbox 不再展示 trigger_root 但保留其余用于运行时兼容)
175
+ function buildTriggerDefinitionBlockDefs() {
176
+ return [
177
+ {
178
+ type: BLOCK_TYPES.triggerRoot,
179
+ message0: "触发器条件",
180
+ message1: "满足条件 %1",
181
+ args1: [
182
+ { type: "input_statement", name: INPUT_NAMES.tree }
183
+ ],
184
+ message2: "关闭条件 %1",
185
+ args2: [
186
+ { type: "input_statement", name: INPUT_NAMES.closeWhen }
187
+ ],
188
+ message3: "关闭方式 %1",
189
+ args3: [
190
+ {
191
+ type: "field_dropdown",
192
+ name: FIELD_NAMES.closeMode,
193
+ options: CLOSE_MODE_OPTIONS.map((p) => [p[0], p[1]])
194
+ }
195
+ ],
196
+ style: BLOCK_STYLE_KEYS.container,
197
+ tooltip: "由玩家触发引擎直接求值的条件定义",
198
+ helpUrl: ""
199
+ },
200
+ {
201
+ type: BLOCK_TYPES.opAnd,
202
+ message0: "并且(全部满足)",
203
+ style: BLOCK_STYLE_KEYS.boolean,
204
+ previousStatement: null,
205
+ nextStatement: null,
206
+ mutator: MUTATOR_OP_CHILDREN,
207
+ tooltip: "所有子条件都满足",
208
+ helpUrl: ""
209
+ },
210
+ {
211
+ type: BLOCK_TYPES.opOr,
212
+ message0: "或者(任一满足)",
213
+ style: BLOCK_STYLE_KEYS.boolean,
214
+ previousStatement: null,
215
+ nextStatement: null,
216
+ mutator: MUTATOR_OP_CHILDREN,
217
+ tooltip: "任意一个子条件满足",
218
+ helpUrl: ""
219
+ },
220
+ {
221
+ type: BLOCK_TYPES.opNot,
222
+ message0: "不满足 %1",
223
+ args0: [
224
+ { type: "input_statement", name: INPUT_NAMES.child }
225
+ ],
226
+ style: BLOCK_STYLE_KEYS.boolean,
227
+ previousStatement: null,
228
+ nextStatement: null,
229
+ tooltip: "对子条件取反",
230
+ helpUrl: ""
231
+ },
232
+ {
233
+ type: BLOCK_TYPES.leafTime,
234
+ extensions: [EXT_LEAF_TIME_INIT],
235
+ tooltip: "世界时间到达指定时刻",
236
+ helpUrl: ""
237
+ },
238
+ {
239
+ type: BLOCK_TYPES.leafEvent,
240
+ extensions: [EXT_LEAF_EVENT_DROPDOWN],
241
+ tooltip: "事件发生后可延迟若干剧情秒",
242
+ helpUrl: ""
243
+ },
244
+ {
245
+ type: BLOCK_TYPES.leafNl,
246
+ message0: "达到条件 %1 里程碑 %2",
247
+ args0: [
248
+ {
249
+ type: FIELD_MULTILINE_TEXT,
250
+ name: FIELD_NAMES.text,
251
+ text: "",
252
+ spellcheck: false,
253
+ maxLines: LEAF_NL_DEFAULT_MAX_LINES
254
+ },
255
+ {
256
+ type: "field_checkbox",
257
+ name: FIELD_NAMES.milestone,
258
+ checked: false
259
+ }
260
+ ],
261
+ message1: "延迟(剧情秒) %1",
262
+ args1: [
263
+ {
264
+ type: "field_number",
265
+ name: FIELD_NAMES.delaySeconds,
266
+ value: 0,
267
+ min: 0
268
+ }
269
+ ],
270
+ style: BLOCK_STYLE_KEYS.dataEvent,
271
+ previousStatement: null,
272
+ nextStatement: null,
273
+ tooltip: "用自然语言描述需要达到的剧情条件",
274
+ helpUrl: ""
275
+ },
276
+ {
277
+ type: BLOCK_TYPES.leafAttrRef,
278
+ extensions: [EXT_LEAF_ATTR_REF_DROPDOWN],
279
+ tooltip: "legacy: entity attribute reference",
280
+ helpUrl: ""
281
+ },
282
+ {
283
+ type: BLOCK_TYPES.leafRelationRef,
284
+ message0: "关系 %1 → %2 类型 %3 属性 %4 期望值 %5 延迟(剧情秒) %6",
285
+ args0: [
286
+ { type: "field_input", name: FIELD_NAMES.srcId, text: "" },
287
+ { type: "field_input", name: FIELD_NAMES.dstId, text: "" },
288
+ { type: "field_input", name: FIELD_NAMES.kind, text: "" },
289
+ { type: "field_input", name: FIELD_NAMES.attrKey, text: "" },
290
+ { type: "field_input", name: FIELD_NAMES.expectedValue, text: "" },
291
+ { type: "field_number", name: FIELD_NAMES.delaySeconds, value: 0, min: 0 }
292
+ ],
293
+ style: BLOCK_STYLE_KEYS.dataAttr,
294
+ previousStatement: null,
295
+ nextStatement: null,
296
+ tooltip: "legacy: relation attribute reference",
297
+ helpUrl: ""
298
+ },
299
+ {
300
+ type: BLOCK_TYPES.leafDaySegment,
301
+ message0: "当前时段为 %1",
302
+ args0: [
303
+ {
304
+ type: "field_dropdown",
305
+ name: FIELD_NAMES.segment,
306
+ options: [
307
+ ["清晨", "morning"],
308
+ ["上午", "forenoon"],
309
+ ["下午", "afternoon"],
310
+ ["夜晚", "night"]
311
+ ]
312
+ }
313
+ ],
314
+ style: BLOCK_STYLE_KEYS.time,
315
+ previousStatement: null,
316
+ nextStatement: null,
317
+ tooltip: "世界时间处于指定语义时段",
318
+ helpUrl: ""
319
+ }
320
+ ];
321
+ }
322
+ // Scratch 风格 hat block (顶部尖角 无 previousStatement) 新类型进 toolbox 旧类型保留兼容
323
+ function buildHatBlockDefs() {
324
+ return [
325
+ {
326
+ type: "when_scenario_start",
327
+ message0: "剧本启动时",
328
+ nextStatement: null,
329
+ style: BLOCK_STYLE_KEYS.scenarioHat,
330
+ tooltip: "剧本启动时执行"
331
+ },
332
+ {
333
+ type: "when_value_changed",
334
+ message0: "当 %1 变化时",
335
+ args0: [{ type: "input_value", name: "VALUE" }],
336
+ inputsInline: true,
337
+ nextStatement: null,
338
+ style: BLOCK_STYLE_KEYS.scenarioHat,
339
+ tooltip: "变量值或对象属性值变化时执行"
340
+ },
341
+ {
342
+ type: "when_time_changed",
343
+ message0: "检测到时间变化",
344
+ nextStatement: null,
345
+ style: BLOCK_STYLE_KEYS.scenarioHat,
346
+ tooltip: "世界时间发生正向或反向变化时执行"
347
+ },
348
+ {
349
+ type: "when_entity_state_changed",
350
+ message0: "当对象 %1 状态变为 %2 时",
351
+ args0: [
352
+ { type: "input_value", name: "OBJECT", check: "Object" },
353
+ { type: "input_value", name: "STATE", check: "Boolean" }
354
+ ],
355
+ inputsInline: true,
356
+ nextStatement: null,
357
+ style: BLOCK_STYLE_KEYS.scenarioHat,
358
+ tooltip: "对象状态切换时执行"
359
+ },
360
+ {
361
+ type: "when_var_changed",
362
+ message0: "当变量 %1 变化时",
363
+ args0: [{ type: "field_variable", name: "VAR", variable: "var" }],
364
+ nextStatement: null,
365
+ style: BLOCK_STYLE_KEYS.scenarioHat,
366
+ tooltip: "变量值变化时执行"
367
+ },
368
+ {
369
+ type: "when_time_reached",
370
+ message0: "当时间到达 %1 时",
371
+ args0: [{ type: "input_value", name: "TIME", check: "Number" }],
372
+ inputsInline: true,
373
+ nextStatement: null,
374
+ style: BLOCK_STYLE_KEYS.scenarioHat,
375
+ tooltip: "世界时间到达指定点时执行"
376
+ },
377
+ {
378
+ type: "when_recall_success",
379
+ message0: "当召回成功时",
380
+ nextStatement: null,
381
+ style: BLOCK_STYLE_KEYS.scenarioHat,
382
+ tooltip: "LLM 检索并召回成功时执行"
383
+ }
384
+ ];
385
+ }
386
+ // 控制流 statement block — 7 个
387
+ function buildControlBlockDefs() {
388
+ return [
389
+ {
390
+ type: "if",
391
+ message0: "如果 %1 那么",
392
+ args0: [{ type: "input_value", name: "COND", check: "Boolean" }],
393
+ message1: "%1",
394
+ args1: [{ type: "input_statement", name: "THEN" }],
395
+ previousStatement: null,
396
+ nextStatement: null,
397
+ style: BLOCK_STYLE_KEYS.control,
398
+ tooltip: "条件为真时执行分支"
399
+ },
400
+ {
401
+ type: "if_else",
402
+ message0: "如果 %1 那么",
403
+ args0: [{ type: "input_value", name: "COND", check: "Boolean" }],
404
+ message1: "%1",
405
+ args1: [{ type: "input_statement", name: "THEN" }],
406
+ message2: "否则 %1",
407
+ args2: [{ type: "input_statement", name: "ELSE" }],
408
+ previousStatement: null,
409
+ nextStatement: null,
410
+ style: BLOCK_STYLE_KEYS.control,
411
+ tooltip: "条件为真则执行 THEN 否则 ELSE"
412
+ },
413
+ {
414
+ type: "repeat",
415
+ message0: "重复 %1 次",
416
+ args0: [{ type: "input_value", name: "TIMES", check: "Number" }],
417
+ message1: "%1",
418
+ args1: [{ type: "input_statement", name: "BODY" }],
419
+ previousStatement: null,
420
+ nextStatement: null,
421
+ style: BLOCK_STYLE_KEYS.control,
422
+ tooltip: "重复指定次数"
423
+ },
424
+ {
425
+ type: "repeat_until",
426
+ message0: "重复直到 %1",
427
+ args0: [{ type: "input_value", name: "UNTIL", check: "Boolean" }],
428
+ message1: "%1",
429
+ args1: [{ type: "input_statement", name: "BODY" }],
430
+ previousStatement: null,
431
+ nextStatement: null,
432
+ style: BLOCK_STYLE_KEYS.control,
433
+ tooltip: "循环直到条件为真"
434
+ },
435
+ {
436
+ type: "wait_seconds",
437
+ message0: "等待 %1 %2",
438
+ args0: [
439
+ { type: "input_value", name: "AMOUNT", check: "Number" },
440
+ {
441
+ type: "field_dropdown",
442
+ name: "UNIT",
443
+ options: [
444
+ ["秒", "second"],
445
+ ["分", "minute"],
446
+ ["时", "hour"]
447
+ ]
448
+ }
449
+ ],
450
+ previousStatement: null,
451
+ nextStatement: null,
452
+ inputsInline: true,
453
+ style: BLOCK_STYLE_KEYS.control,
454
+ tooltip: "暂停指定时长"
455
+ },
456
+ {
457
+ type: "stop_this_script",
458
+ message0: "停止此脚本",
459
+ previousStatement: null,
460
+ style: BLOCK_STYLE_KEYS.control,
461
+ tooltip: "结束当前规则 无 next 卡口"
462
+ }
463
+ ];
464
+ }
465
+ // 传感 reporter block — 9 个
466
+ function buildSensingBlockDefs() {
467
+ return [
468
+ {
469
+ type: "sense_time_year",
470
+ message0: "当前年",
471
+ output: "Number",
472
+ style: BLOCK_STYLE_KEYS.sensing,
473
+ tooltip: "当前世界年"
474
+ },
475
+ {
476
+ type: "sense_time_month",
477
+ message0: "当前月",
478
+ output: "Number",
479
+ style: BLOCK_STYLE_KEYS.sensing,
480
+ tooltip: "当前世界月"
481
+ },
482
+ {
483
+ type: "sense_time_day",
484
+ message0: "当前日",
485
+ output: "Number",
486
+ style: BLOCK_STYLE_KEYS.sensing,
487
+ tooltip: "当前世界日"
488
+ },
489
+ {
490
+ type: "sense_time_hour",
491
+ message0: "当前时",
492
+ output: "Number",
493
+ style: BLOCK_STYLE_KEYS.sensing,
494
+ tooltip: "当前世界时"
495
+ },
496
+ {
497
+ type: "sense_time_minute",
498
+ message0: "当前分",
499
+ output: "Number",
500
+ style: BLOCK_STYLE_KEYS.sensing,
501
+ tooltip: "当前世界分"
502
+ },
503
+ {
504
+ type: "sense_time_second",
505
+ message0: "当前秒",
506
+ output: "Number",
507
+ style: BLOCK_STYLE_KEYS.sensing,
508
+ tooltip: "当前世界秒"
509
+ },
510
+ {
511
+ type: "sense_object_state",
512
+ message0: "对象 %1 状态",
513
+ args0: [{ type: FIELD_ENTITY_PICKER, name: "ENTITY" }],
514
+ output: "Object",
515
+ style: BLOCK_STYLE_KEYS.sensing,
516
+ tooltip: "对象引用 (方形输出 可插入需要对象槽位)"
517
+ },
518
+ {
519
+ type: "sense_object_attr",
520
+ message0: "对象 %1 的属性 %2",
521
+ args0: [
522
+ { type: FIELD_ENTITY_PICKER, name: "ENTITY" },
523
+ { type: FIELD_ATTR_PICKER, name: "ATTR" }
524
+ ],
525
+ output: null,
526
+ inputsInline: true,
527
+ style: BLOCK_STYLE_KEYS.sensing,
528
+ tooltip: "读取对象属性当前值 可插入变化事件的圆角值槽"
529
+ },
530
+ {
531
+ type: "sense_edge_relation",
532
+ message0: "对象 %1 可见",
533
+ args0: [{ type: FIELD_ENTITY_PICKER, name: "ENTITY" }],
534
+ output: "Boolean",
535
+ style: BLOCK_STYLE_KEYS.sensing,
536
+ tooltip: "读取对象当前是否可见 可见返回 true 不可见返回 false"
537
+ },
538
+ {
539
+ type: "sense_relation_ref",
540
+ message0: "关系 %1 → %2 类型 %3",
541
+ args0: [
542
+ { type: FIELD_ENTITY_PICKER, name: "SRC_ENTITY" },
543
+ { type: FIELD_ENTITY_PICKER, name: "DST_ENTITY" },
544
+ { type: "field_input", name: "RELATION_KIND", text: "" }
545
+ ],
546
+ message1: "属性 JSON Pointer %1 期望 JSON %2",
547
+ args1: [
548
+ { type: "field_input", name: "ATTRIBUTE_PATH", text: "" },
549
+ { type: "field_input", name: "EXPECTED_JSON", text: "" }
550
+ ],
551
+ message2: "持续剧情秒 %1",
552
+ args2: [
553
+ { type: "field_number", name: "DELAY_WORLD_SECONDS", value: 0, min: 0, precision: 1 }
554
+ ],
555
+ output: "Boolean",
556
+ inputsInline: false,
557
+ style: BLOCK_STYLE_KEYS.sensing,
558
+ tooltip: "按源实体、目标实体和关系类型读取真实关系;可用分段属性路径、JSON 期望值和剧情时间延迟限定"
559
+ },
560
+ {
561
+ type: "sense_object_frozen",
562
+ message0: "对象 %1 已冻结",
563
+ args0: [{ type: FIELD_ENTITY_PICKER, name: "ENTITY" }],
564
+ output: "Boolean",
565
+ style: BLOCK_STYLE_KEYS.sensing,
566
+ tooltip: "读取对象当前是否被冻结 被冻结返回 true 未被冻结返回 false"
567
+ }
568
+ ];
569
+ }
570
+ // 运算 reporter block (boolean/number/string) — 13 个
571
+ function buildOperatorBlockDefs() {
572
+ return [
573
+ {
574
+ type: "op_time_crossed",
575
+ message0: "本次时间变动 %1 跨过 %2",
576
+ args0: [
577
+ {
578
+ type: "field_dropdown",
579
+ name: "DIRECTION",
580
+ options: [
581
+ ["任意方向", "any"],
582
+ ["仅正向", "forward"],
583
+ ["仅反向", "backward"]
584
+ ]
585
+ },
586
+ { type: "input_value", name: "TIME", check: "Number" }
587
+ ],
588
+ output: "Boolean",
589
+ inputsInline: true,
590
+ style: BLOCK_STYLE_KEYS.operators,
591
+ tooltip: "判断触发本次时间变化事件的区间是否跨过目标时间点"
592
+ },
593
+ {
594
+ type: "op_and",
595
+ message0: "%1 且 %2",
596
+ args0: [
597
+ { type: "input_value", name: "A", check: "Boolean" },
598
+ { type: "input_value", name: "B", check: "Boolean" }
599
+ ],
600
+ output: "Boolean",
601
+ inputsInline: true,
602
+ style: BLOCK_STYLE_KEYS.operators,
603
+ tooltip: "逻辑与"
604
+ },
605
+ {
606
+ type: "op_or",
607
+ message0: "%1 或 %2",
608
+ args0: [
609
+ { type: "input_value", name: "A", check: "Boolean" },
610
+ { type: "input_value", name: "B", check: "Boolean" }
611
+ ],
612
+ output: "Boolean",
613
+ inputsInline: true,
614
+ style: BLOCK_STYLE_KEYS.operators,
615
+ tooltip: "逻辑或"
616
+ },
617
+ {
618
+ type: "op_not",
619
+ message0: "非 %1",
620
+ args0: [{ type: "input_value", name: "A", check: "Boolean" }],
621
+ output: "Boolean",
622
+ inputsInline: true,
623
+ style: BLOCK_STYLE_KEYS.operators,
624
+ tooltip: "逻辑非"
625
+ },
626
+ {
627
+ type: "op_equals",
628
+ message0: "%1 = %2",
629
+ args0: [
630
+ { type: "input_value", name: "A" },
631
+ { type: "input_value", name: "B" }
632
+ ],
633
+ output: "Boolean",
634
+ inputsInline: true,
635
+ style: BLOCK_STYLE_KEYS.operators,
636
+ tooltip: "两值相等"
637
+ },
638
+ {
639
+ type: "op_greater",
640
+ message0: "%1 > %2",
641
+ args0: [
642
+ { type: "input_value", name: "A", check: "Number" },
643
+ { type: "input_value", name: "B", check: "Number" }
644
+ ],
645
+ output: "Boolean",
646
+ inputsInline: true,
647
+ style: BLOCK_STYLE_KEYS.operators,
648
+ tooltip: "左值大于右值"
649
+ },
650
+ {
651
+ type: "op_less",
652
+ message0: "%1 < %2",
653
+ args0: [
654
+ { type: "input_value", name: "A", check: "Number" },
655
+ { type: "input_value", name: "B", check: "Number" }
656
+ ],
657
+ output: "Boolean",
658
+ inputsInline: true,
659
+ style: BLOCK_STYLE_KEYS.operators,
660
+ tooltip: "左值小于右值"
661
+ },
662
+ {
663
+ type: "op_contains",
664
+ message0: "%1 包含 %2",
665
+ args0: [
666
+ { type: "input_value", name: "A", check: "String" },
667
+ { type: "input_value", name: "B", check: "String" }
668
+ ],
669
+ output: "Boolean",
670
+ inputsInline: true,
671
+ style: BLOCK_STYLE_KEYS.operators,
672
+ tooltip: "字符串包含子串"
673
+ },
674
+ {
675
+ type: "op_add",
676
+ message0: "%1 + %2",
677
+ args0: [
678
+ { type: "input_value", name: "A", check: "Number" },
679
+ { type: "input_value", name: "B", check: "Number" }
680
+ ],
681
+ output: "Number",
682
+ inputsInline: true,
683
+ style: BLOCK_STYLE_KEYS.operators,
684
+ tooltip: "加法"
685
+ },
686
+ {
687
+ type: "op_subtract",
688
+ message0: "%1 - %2",
689
+ args0: [
690
+ { type: "input_value", name: "A", check: "Number" },
691
+ { type: "input_value", name: "B", check: "Number" }
692
+ ],
693
+ output: "Number",
694
+ inputsInline: true,
695
+ style: BLOCK_STYLE_KEYS.operators,
696
+ tooltip: "减法"
697
+ },
698
+ {
699
+ type: "op_multiply",
700
+ message0: "%1 × %2",
701
+ args0: [
702
+ { type: "input_value", name: "A", check: "Number" },
703
+ { type: "input_value", name: "B", check: "Number" }
704
+ ],
705
+ output: "Number",
706
+ inputsInline: true,
707
+ style: BLOCK_STYLE_KEYS.operators,
708
+ tooltip: "乘法"
709
+ },
710
+ {
711
+ type: "op_divide",
712
+ message0: "%1 ÷ %2",
713
+ args0: [
714
+ { type: "input_value", name: "A", check: "Number" },
715
+ { type: "input_value", name: "B", check: "Number" }
716
+ ],
717
+ output: "Number",
718
+ inputsInline: true,
719
+ style: BLOCK_STYLE_KEYS.operators,
720
+ tooltip: "除法"
721
+ },
722
+ {
723
+ type: "op_mod",
724
+ message0: "%1 mod %2",
725
+ args0: [
726
+ { type: "input_value", name: "A", check: "Number" },
727
+ { type: "input_value", name: "B", check: "Number" }
728
+ ],
729
+ output: "Number",
730
+ inputsInline: true,
731
+ style: BLOCK_STYLE_KEYS.operators,
732
+ tooltip: "取余"
733
+ },
734
+ {
735
+ type: "op_round",
736
+ message0: "四舍五入 %1",
737
+ args0: [{ type: "input_value", name: "A", check: "Number" }],
738
+ output: "Number",
739
+ inputsInline: true,
740
+ style: BLOCK_STYLE_KEYS.operators,
741
+ tooltip: "四舍五入"
742
+ },
743
+ {
744
+ type: "op_abs",
745
+ message0: "绝对值 %1",
746
+ args0: [{ type: "input_value", name: "A", check: "Number" }],
747
+ output: "Number",
748
+ inputsInline: true,
749
+ style: BLOCK_STYLE_KEYS.operators,
750
+ tooltip: "绝对值"
751
+ },
752
+ {
753
+ type: "op_join",
754
+ message0: "拼接 %1 %2",
755
+ args0: [
756
+ { type: "input_value", name: "A", check: "String" },
757
+ { type: "input_value", name: "B", check: "String" }
758
+ ],
759
+ output: "String",
760
+ inputsInline: true,
761
+ style: BLOCK_STYLE_KEYS.operators,
762
+ tooltip: "字符串拼接"
763
+ },
764
+ {
765
+ type: "op_letter_of",
766
+ message0: "%1 的第 %2 个字符",
767
+ args0: [
768
+ { type: "input_value", name: "STRING", check: "String" },
769
+ { type: "input_value", name: "INDEX", check: "Number" }
770
+ ],
771
+ output: "String",
772
+ inputsInline: true,
773
+ style: BLOCK_STYLE_KEYS.operators,
774
+ tooltip: "取字符串第 N 个字符"
775
+ },
776
+ {
777
+ type: "op_length_of",
778
+ message0: "%1 的长度",
779
+ args0: [{ type: "input_value", name: "STRING", check: "String" }],
780
+ output: "Number",
781
+ inputsInline: true,
782
+ style: BLOCK_STYLE_KEYS.operators,
783
+ tooltip: "字符串长度"
784
+ }
785
+ ];
786
+ }
787
+ // 变量 + 实体操作 + 时间 statement/reporter — 12 个
788
+ function buildVariableEntityBlockDefs() {
789
+ return [
790
+ {
791
+ type: "var_read",
792
+ message0: "变量 %1",
793
+ args0: [{ type: "field_variable", name: "NAME", variable: "var" }],
794
+ output: "String",
795
+ inputsInline: true,
796
+ style: BLOCK_STYLE_KEYS.variables,
797
+ tooltip: "读取变量"
798
+ },
799
+ {
800
+ type: "var_set",
801
+ message0: "设 %1 为 %2",
802
+ args0: [
803
+ { type: "field_variable", name: "NAME", variable: "var" },
804
+ { type: "input_value", name: "VALUE" }
805
+ ],
806
+ previousStatement: null,
807
+ nextStatement: null,
808
+ inputsInline: true,
809
+ style: BLOCK_STYLE_KEYS.variables,
810
+ tooltip: "设置变量值"
811
+ },
812
+ {
813
+ type: "var_change",
814
+ message0: "让 %1 增加 %2",
815
+ args0: [
816
+ { type: "field_variable", name: "NAME", variable: "var" },
817
+ { type: "input_value", name: "VALUE", check: "Number" }
818
+ ],
819
+ previousStatement: null,
820
+ nextStatement: null,
821
+ inputsInline: true,
822
+ style: BLOCK_STYLE_KEYS.variables,
823
+ tooltip: "增量变量"
824
+ },
825
+ {
826
+ type: "entity_freeze",
827
+ message0: "冻结对象 %1 当前状态",
828
+ args0: [{ type: FIELD_ENTITY_PICKER, name: "ENTITY" }],
829
+ previousStatement: null,
830
+ nextStatement: null,
831
+ style: BLOCK_STYLE_KEYS.variables,
832
+ tooltip: "冻结对象当前状态 (后续不再变化)"
833
+ },
834
+ {
835
+ type: "entity_toggle",
836
+ message0: "切换对象 %1 可见状态",
837
+ args0: [{ type: FIELD_ENTITY_PICKER, name: "ENTITY" }],
838
+ previousStatement: null,
839
+ nextStatement: null,
840
+ style: BLOCK_STYLE_KEYS.variables,
841
+ tooltip: "切换对象的可见 / 不可见"
842
+ },
843
+ {
844
+ type: "entity_enable",
845
+ message0: "将对象 %1 设置为可见",
846
+ args0: [{ type: FIELD_ENTITY_PICKER, name: "ENTITY" }],
847
+ previousStatement: null,
848
+ nextStatement: null,
849
+ style: BLOCK_STYLE_KEYS.variables,
850
+ tooltip: "将对象设置为可见"
851
+ },
852
+ {
853
+ type: "entity_disable",
854
+ message0: "将对象 %1 设置为不可见",
855
+ args0: [{ type: FIELD_ENTITY_PICKER, name: "ENTITY" }],
856
+ previousStatement: null,
857
+ nextStatement: null,
858
+ style: BLOCK_STYLE_KEYS.variables,
859
+ tooltip: "将对象设置为不可见"
860
+ },
861
+ {
862
+ type: "attr_set",
863
+ message0: "设 %1 的属性 %2 为 %3",
864
+ args0: [
865
+ { type: FIELD_ENTITY_PICKER, name: "ENTITY" },
866
+ { type: FIELD_ATTR_PICKER, name: "ATTR" },
867
+ { type: "input_value", name: "VALUE" }
868
+ ],
869
+ previousStatement: null,
870
+ nextStatement: null,
871
+ inputsInline: true,
872
+ style: BLOCK_STYLE_KEYS.variables,
873
+ tooltip: "设置对象属性"
874
+ },
875
+ {
876
+ type: "time_advance",
877
+ message0: "推进时间 %1 %2",
878
+ args0: [
879
+ { type: "field_number", name: "AMOUNT", value: 0 },
880
+ {
881
+ type: "field_dropdown",
882
+ name: "UNIT",
883
+ options: [
884
+ ["年", "year"],
885
+ ["月", "month"],
886
+ ["日", "day"],
887
+ ["时", "hour"],
888
+ ["分", "minute"],
889
+ ["秒", "second"]
890
+ ]
891
+ }
892
+ ],
893
+ previousStatement: null,
894
+ nextStatement: null,
895
+ inputsInline: true,
896
+ style: BLOCK_STYLE_KEYS.variables,
897
+ tooltip: "世界时间前进 N 单位 (负数倒退)"
898
+ },
899
+ {
900
+ type: "time_set",
901
+ message0: "设时间为 %1",
902
+ args0: [{ type: "input_value", name: "TIME", check: "Number" }],
903
+ previousStatement: null,
904
+ nextStatement: null,
905
+ inputsInline: true,
906
+ style: BLOCK_STYLE_KEYS.variables,
907
+ tooltip: "设置世界时间 (可插入时间常量或时间戳)"
908
+ },
909
+ {
910
+ type: "var_const_boolean",
911
+ message0: "状态常量 %1",
912
+ args0: [
913
+ {
914
+ type: "field_dropdown",
915
+ name: "VALUE",
916
+ options: [
917
+ ["开启", "true"],
918
+ ["关闭", "false"]
919
+ ]
920
+ }
921
+ ],
922
+ output: "Boolean",
923
+ style: BLOCK_STYLE_KEYS.variables,
924
+ tooltip: "布尔常量 (尖角输出)"
925
+ },
926
+ {
927
+ type: "var_const_value",
928
+ message0: "数字或文本常量 %1",
929
+ args0: [{ type: "field_input", name: "VALUE", text: "" }],
930
+ output: null,
931
+ style: BLOCK_STYLE_KEYS.variables,
932
+ tooltip: "数字或字符串常量 (圆角输出)"
933
+ },
934
+ {
935
+ type: "var_const_time",
936
+ message0: "%1 年 %2 月 %3 日 %4 时 %5 分 %6 秒",
937
+ args0: [
938
+ { type: "field_number", name: "YEAR", value: 0, min: 0 },
939
+ { type: "field_number", name: "MONTH", value: 1, min: 1, max: 12 },
940
+ { type: "field_number", name: "DAY", value: 1, min: 1, max: 31 },
941
+ { type: "field_number", name: "HOUR", value: 0, min: 0, max: 23 },
942
+ { type: "field_number", name: "MINUTE", value: 0, min: 0, max: 59 },
943
+ { type: "field_number", name: "SECOND", value: 0, min: 0, max: 59 }
944
+ ],
945
+ output: "Number",
946
+ inputsInline: true,
947
+ style: BLOCK_STYLE_KEYS.variables,
948
+ tooltip: "时间常量 (6 数字槽 输出大数时间戳)"
949
+ }
950
+ ];
951
+ }
952
+ let blocksRegistered = false;
953
+ const TRIGGER_BLOCK_REGISTRY_MARKER = Symbol.for("@world-engines/blocks-editor/trigger-block-registry");
954
+ // Any change to the registered block JSON or extension semantics must bump this value.
955
+ // Mixed versions intentionally fail closed and require a full page reload.
956
+ const TRIGGER_BLOCK_REGISTRY_VERSION = "trigger-blocks-v1";
957
+ export function registerTriggerBlocks() {
958
+ registerMultilineFieldOnce();
959
+ registerExtensionsOnce();
960
+ registerPickerFieldsOnce();
961
+ if (blocksRegistered)
962
+ return;
963
+ const defs = [
964
+ ...buildTriggerDefinitionBlockDefs(),
965
+ ...buildHatBlockDefs(),
966
+ ...buildControlBlockDefs(),
967
+ ...buildSensingBlockDefs(),
968
+ ...buildOperatorBlockDefs(),
969
+ ...buildVariableEntityBlockDefs()
970
+ ];
971
+ const registry = Blockly.Blocks;
972
+ const marker = registry[TRIGGER_BLOCK_REGISTRY_MARKER];
973
+ const registeredCount = defs.reduce((count, def) => count + (Object.prototype.hasOwnProperty.call(Blockly.Blocks, def.type) ? 1 : 0), 0);
974
+ if (marker === TRIGGER_BLOCK_REGISTRY_VERSION) {
975
+ if (registeredCount !== defs.length) {
976
+ throw new Error(`trigger_block_registry_incomplete:${registeredCount}/${defs.length}`);
977
+ }
978
+ blocksRegistered = true;
979
+ return;
980
+ }
981
+ if (marker !== undefined || registeredCount > 0) {
982
+ throw new Error(`trigger_block_registry_conflict:${registeredCount}/${defs.length}`);
983
+ }
984
+ Blockly.common.defineBlocksWithJsonArray(defs);
985
+ Object.defineProperty(registry, TRIGGER_BLOCK_REGISTRY_MARKER, {
986
+ configurable: false,
987
+ enumerable: false,
988
+ value: TRIGGER_BLOCK_REGISTRY_VERSION,
989
+ writable: false
990
+ });
991
+ blocksRegistered = true;
992
+ }
993
+ export function listTriggerBlockTypes() {
994
+ return Object.values(BLOCK_TYPES);
995
+ }
996
+ export function isTriggerBlockRegistered(type) {
997
+ return Object.prototype.hasOwnProperty.call(Blockly.Blocks, type);
998
+ }
999
+ // 导出全部 hat block type 供 adapter 识别
1000
+ export { HAT_BLOCK_TYPES, HAT_TYPES, isHatType, CONTROL_STATEMENT_TYPES, SENSING_REPORTER_TYPES, OPERATOR_BOOLEAN_TYPES, OPERATOR_NUMBER_TYPES, OPERATOR_STRING_TYPES, VARIABLE_TYPES, ENTITY_OP_TYPES, TIME_OP_TYPES } from "./registry-taxonomy.js";