@silkweaver/build 1.0.0 → 1.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.
package/dist/build.js CHANGED
@@ -54,6 +54,7 @@ const fs = __importStar(require("node:fs"));
54
54
  const path = __importStar(require("node:path"));
55
55
  const os = __importStar(require("node:os"));
56
56
  const node_url_1 = require("node:url");
57
+ const object_format_js_1 = require("./object_format.js");
57
58
  /** Reads and parses a project's project.json. */
58
59
  async function read_project(project_folder) {
59
60
  const proj_text = await fs.promises.readFile(path.join(project_folder, 'project.json'), 'utf8');
@@ -101,7 +102,8 @@ function hex_to_bgr(hex) {
101
102
  * @param asset_mode - 'preview' (file:// into project) or 'export' (relative assets/)
102
103
  */
103
104
  async function generate_entry_code(project_folder, proj, asset_mode) {
104
- const object_names = Object.keys(proj.resources.objects ?? {});
105
+ // Parent-before-child so generated imports/registrations init objects in dependency order.
106
+ const object_names = await _objects_parent_first(project_folder, Object.keys(proj.resources.objects ?? {}));
105
107
  const room_names = Object.keys(proj.resources.rooms ?? {});
106
108
  const sprite_names = Object.keys(proj.resources.sprites ?? {});
107
109
  const script_names = Object.keys(proj.resources.scripts ?? {});
@@ -113,10 +115,12 @@ async function generate_entry_code(project_folder, proj, asset_mode) {
113
115
  // Each object is a single class file: objects/<name>.ts (a gm_object subclass).
114
116
  // It is imported as-is; metadata/variables/events all live in the class.
115
117
  const object_imports = [];
118
+ const object_registrations = []; // object_register_name('x', x) → resolves object_get('x')
116
119
  for (const obj_name of object_names) {
117
120
  const class_file = path.join(project_folder, 'objects', `${obj_name}.ts`);
118
121
  if (await _path_exists(class_file)) {
119
122
  object_imports.push(`import { ${obj_name} } from '${class_file.replace(/\\/g, '/')}'`);
123
+ object_registrations.push(`object_register_name('${obj_name}', ${obj_name})`);
120
124
  }
121
125
  else {
122
126
  console.warn(`[build] object '${obj_name}' has no objects/${obj_name}.ts — skipped`);
@@ -323,6 +327,9 @@ ${engine_import}
323
327
 
324
328
  ${object_imports.join('\n')}
325
329
 
330
+ // Register objects by name so object_get('name') resolves them at runtime.
331
+ ${object_registrations.join('\n')}
332
+
326
333
  ${script_imports}
327
334
 
328
335
  // ── Sprite loader ───────────────────────────────────────────────────────────
@@ -494,7 +501,23 @@ async function bundle_game(project_folder, proj, asset_mode, out_path, minify) {
494
501
  // `draw_sprite`) resolve to an auto-injected import — tree-shaken — so users never write or
495
502
  // manage `import … from '@silkweaver/engine'` themselves.
496
503
  const engine_names = await engine_export_names(engine_entry());
497
- await fs.promises.writeFile(globals_path, `export { ${engine_names.join(', ')} } from '@silkweaver/engine'\n`, 'utf8');
504
+ // Also re-export the project's OBJECT classes by name, so they can be referenced bare —
505
+ // GMS-style, e.g. `place_meeting(x, y, obj_wall)` or `static parent = par_solid` — with no
506
+ // import (matching what the IDE editor already declares). The defining file won't self-import;
507
+ // skip any object whose name would collide with an engine export.
508
+ // Parent-before-child order: the re-export order here becomes the module init order, so an
509
+ // object's `static parent = par` resolves to a defined class instead of undefined.
510
+ const object_order = await _objects_parent_first(project_folder, Object.keys(proj.resources.objects ?? {}));
511
+ const object_lines = [];
512
+ for (const obj_name of object_order) {
513
+ if (engine_names.includes(obj_name))
514
+ continue;
515
+ const class_file = path.join(project_folder, 'objects', `${obj_name}.ts`);
516
+ if (await _path_exists(class_file)) {
517
+ object_lines.push(`export { ${obj_name} } from '${class_file.replace(/\\/g, '/')}'`);
518
+ }
519
+ }
520
+ await fs.promises.writeFile(globals_path, `export { ${engine_names.join(', ')} } from '@silkweaver/engine'\n${object_lines.join('\n')}\n`, 'utf8');
498
521
  try {
499
522
  // Run esbuild via its Node API. Alias the package specifier to the resolved engine
500
523
  // entry so the generated entry AND any class-per-object files resolve to the same
@@ -506,6 +529,10 @@ async function bundle_game(project_folder, proj, asset_mode, out_path, minify) {
506
529
  outfile: out_path,
507
530
  format: 'esm',
508
531
  minify,
532
+ // Preserve class/function `.name` even when minifying — the engine reads
533
+ // `this.constructor.name` (resource.name, room type checks, object_get_name),
534
+ // so identifier mangling would otherwise break exported (minified) games.
535
+ keepNames: true,
509
536
  alias: { '@silkweaver/engine': engine_entry() },
510
537
  inject: [globals_path],
511
538
  });
@@ -686,6 +713,48 @@ async function _path_exists(p) {
686
713
  return false;
687
714
  }
688
715
  }
716
+ /**
717
+ * Orders object names so every object's `static parent` (when it's another project object)
718
+ * appears BEFORE it. esbuild lazy-initializes the object modules (the inject shim and each
719
+ * object form an import cycle), running them in declaration order inside one initializer; a
720
+ * child whose `static parent = par` is evaluated before `par`'s module would capture
721
+ * `undefined`. Initializing parents first makes the reference resolve to the real class.
722
+ * A parent cycle (user error) is broken gracefully so this never loops.
723
+ */
724
+ async function _objects_parent_first(project_folder, names) {
725
+ const present = new Set(names);
726
+ const parent_of = new Map();
727
+ for (const name of names) {
728
+ const file = path.join(project_folder, 'objects', `${name}.ts`);
729
+ let parent;
730
+ if (await _path_exists(file)) {
731
+ try {
732
+ const p = (0, object_format_js_1.parse_object)(await fs.promises.readFile(file, 'utf8')).parent;
733
+ if (p && p !== name && present.has(p))
734
+ parent = p;
735
+ }
736
+ catch { /* unparseable — treat as no parent */ }
737
+ }
738
+ parent_of.set(name, parent);
739
+ }
740
+ const out = [];
741
+ const done = new Set();
742
+ const onstack = new Set();
743
+ const visit = (n) => {
744
+ if (done.has(n) || onstack.has(n))
745
+ return; // onstack guard breaks any parent cycle
746
+ onstack.add(n);
747
+ const p = parent_of.get(n);
748
+ if (p)
749
+ visit(p);
750
+ onstack.delete(n);
751
+ done.add(n);
752
+ out.push(n);
753
+ };
754
+ for (const n of names)
755
+ visit(n);
756
+ return out;
757
+ }
689
758
  /** Recursively copies a directory tree from src to dst. */
690
759
  async function _copy_dir(src, dst) {
691
760
  await fs.promises.mkdir(dst, { recursive: true });
package/dist/index.d.ts CHANGED
@@ -7,7 +7,9 @@
7
7
  * - export_executable → a packaged desktop application
8
8
  *
9
9
  * Also re-exports the class-file object read/write layer (object_format) used by the
10
- * IDE's object editor. Pure Node drives both the IDE (over IPC) and the CLI.
10
+ * IDE's object editor, and the starter-template materializer (templates). Pure Node
11
+ * drives both the IDE (over IPC) and the CLI.
11
12
  */
12
13
  export * from './build.js';
13
14
  export * from './object_format.js';
15
+ export * from './templates.js';
package/dist/index.js CHANGED
@@ -8,7 +8,8 @@
8
8
  * - export_executable → a packaged desktop application
9
9
  *
10
10
  * Also re-exports the class-file object read/write layer (object_format) used by the
11
- * IDE's object editor. Pure Node drives both the IDE (over IPC) and the CLI.
11
+ * IDE's object editor, and the starter-template materializer (templates). Pure Node
12
+ * drives both the IDE (over IPC) and the CLI.
12
13
  */
13
14
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
15
  if (k2 === undefined) k2 = k;
@@ -27,3 +28,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
27
28
  Object.defineProperty(exports, "__esModule", { value: true });
28
29
  __exportStar(require("./build.js"), exports);
29
30
  __exportStar(require("./object_format.js"), exports);
31
+ __exportStar(require("./templates.js"), exports);
@@ -11,6 +11,8 @@
11
11
  */
12
12
  /** Known static-metadata field names an object class may declare. */
13
13
  export type meta_field = 'sprite' | 'solid' | 'visible' | 'persistent' | 'depth' | 'parent';
14
+ /** Canonical GMS-style order of `on_*` event methods (used to keep events ordered in code). */
15
+ export declare const EVENT_ORDER: string[];
14
16
  export interface object_model {
15
17
  class_name: string;
16
18
  sprite: string | null;
@@ -27,6 +29,14 @@ export interface object_model {
27
29
  }
28
30
  /** Extracts the object model from a class-file source. */
29
31
  export declare function parse_object(src: string): object_model;
32
+ /**
33
+ * Collects every instance member reachable through `this.` in a class: declared instance fields
34
+ * *plus* members created/assigned at runtime inside any event (e.g. `this.velocity = 0` in on_create,
35
+ * read in on_step). This drives `this.` autocomplete, so a variable made in one event surfaces in all
36
+ * the others — matching the usual split of constants on the object, runtime state in Create. Static
37
+ * fields and method/event names are excluded (engine members are merged in separately).
38
+ */
39
+ export declare function this_members(src: string): string[];
30
40
  /**
31
41
  * Sets (adds or updates) a static metadata field. `expr` is the raw initializer
32
42
  * text (e.g. `'spr_player'`, `true`, `-5`, `obj_base`).
@@ -38,9 +48,29 @@ export declare function remove_static(src: string, name: meta_field): string;
38
48
  export declare function set_field(src: string, name: string, expr: string): string;
39
49
  /** Removes an instance variable field. */
40
50
  export declare function remove_field(src: string, name: string): string;
41
- /** Adds an `on_*` event method stub if it is not already present. */
51
+ /**
52
+ * Adds an `on_*` event method stub if it is not already present, inserted in canonical event
53
+ * order (before the first existing event method that comes later in EVENT_ORDER).
54
+ */
42
55
  export declare function add_method(src: string, method: string, params?: string, body?: string): string;
43
56
  /** Removes an event method by name. */
44
57
  export declare function remove_method(src: string, method: string): string;
58
+ /**
59
+ * Returns one event method's body as an editable, de-indented buffer, or null if absent/bodyless.
60
+ * @param method - The on_* method to extract (e.g. 'on_step')
61
+ */
62
+ export declare function get_event_body(src: string, method: string): string | null;
63
+ /**
64
+ * Replaces an event method's body with `body` (an edited, de-indented buffer), re-indenting it to
65
+ * the method's level. Returns the full updated source; a no-op if the method is absent.
66
+ * @param method - The on_* method to update
67
+ * @param body - The new body (column-0 / de-indented, as edited)
68
+ */
69
+ export declare function set_event_body(src: string, method: string, body: string): string;
70
+ /**
71
+ * Normalizes a class-per-object file for display in the full code view: variables hoisted above all
72
+ * events, canonical event order, then consistent indentation. Unchanged if there's no class.
73
+ */
74
+ export declare function normalize_object(src: string): string;
45
75
  /** Generates a minimal class-file source for a new object. Imports are auto-managed (none needed). */
46
76
  export declare function scaffold_object(class_name: string): string;
@@ -44,15 +44,33 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  };
45
45
  })();
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.EVENT_ORDER = void 0;
47
48
  exports.parse_object = parse_object;
49
+ exports.this_members = this_members;
48
50
  exports.set_static = set_static;
49
51
  exports.remove_static = remove_static;
50
52
  exports.set_field = set_field;
51
53
  exports.remove_field = remove_field;
52
54
  exports.add_method = add_method;
53
55
  exports.remove_method = remove_method;
56
+ exports.get_event_body = get_event_body;
57
+ exports.set_event_body = set_event_body;
58
+ exports.normalize_object = normalize_object;
54
59
  exports.scaffold_object = scaffold_object;
55
60
  const ts = __importStar(require("typescript"));
61
+ /** Canonical GMS-style order of `on_*` event methods (used to keep events ordered in code). */
62
+ exports.EVENT_ORDER = [
63
+ 'on_create', 'on_destroy',
64
+ 'on_step_begin', 'on_step', 'on_step_end',
65
+ 'on_draw_begin', 'on_draw', 'on_draw_end', 'on_draw_gui',
66
+ 'on_alarm',
67
+ 'on_key_press', 'on_key_release', 'on_key_held',
68
+ 'on_mouse_left_press', 'on_mouse_left_release', 'on_mouse_right_press',
69
+ 'on_collision',
70
+ 'on_room_start', 'on_room_end', 'on_game_start', 'on_game_end',
71
+ 'on_animation_end', 'on_path_end', 'on_outside_room', 'on_intersect_boundary',
72
+ 'on_no_more_lives', 'on_no_more_health', 'on_user',
73
+ ];
56
74
  // =========================================================================
57
75
  // Parsing
58
76
  // =========================================================================
@@ -126,6 +144,42 @@ function _string_literal(node) {
126
144
  return node.text;
127
145
  return null;
128
146
  }
147
+ /**
148
+ * Collects every instance member reachable through `this.` in a class: declared instance fields
149
+ * *plus* members created/assigned at runtime inside any event (e.g. `this.velocity = 0` in on_create,
150
+ * read in on_step). This drives `this.` autocomplete, so a variable made in one event surfaces in all
151
+ * the others — matching the usual split of constants on the object, runtime state in Create. Static
152
+ * fields and method/event names are excluded (engine members are merged in separately).
153
+ */
154
+ function this_members(src) {
155
+ const sf = _source(src);
156
+ const cls = _find_class(sf);
157
+ if (!cls)
158
+ return [];
159
+ const names = new Set();
160
+ for (const m of cls.members) {
161
+ if (ts.isPropertyDeclaration(m) && !_is_static(m)) {
162
+ const n = _name_of(m);
163
+ if (n)
164
+ names.add(n);
165
+ }
166
+ }
167
+ // Any `this.<name> = …` (or compound assignment) anywhere in the class body — including nested
168
+ // blocks/closures — counts as an instance member the user can reference elsewhere.
169
+ const visit = (node) => {
170
+ if (ts.isBinaryExpression(node)
171
+ && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment
172
+ && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
173
+ && ts.isPropertyAccessExpression(node.left)
174
+ && node.left.expression.kind === ts.SyntaxKind.ThisKeyword
175
+ && ts.isIdentifier(node.left.name)) {
176
+ names.add(node.left.name.text);
177
+ }
178
+ ts.forEachChild(node, visit);
179
+ };
180
+ visit(cls);
181
+ return [...names];
182
+ }
129
183
  function _apply(src, e) {
130
184
  return src.slice(0, e.start) + e.text + src.slice(e.end);
131
185
  }
@@ -139,6 +193,20 @@ function _insert_member(src, text) {
139
193
  const prefix = pos > 0 && src[pos - 1] !== '\n' ? '\n' : ''; // guard against mashing onto the prior line
140
194
  return _apply(src, { start: pos, end: pos, text: prefix + text });
141
195
  }
196
+ /** Inserts `text` (a full member incl. trailing newline) on its own line before the member at `pos`. */
197
+ function _insert_before(src, pos, text) {
198
+ const line_start = src.lastIndexOf('\n', pos - 1) + 1;
199
+ return src.slice(0, line_start) + text + src.slice(line_start);
200
+ }
201
+ /** Inserts `text` (a full member incl. trailing newline) on its own line after the member ending at `end`. */
202
+ function _insert_after(src, end, text) {
203
+ let i = end;
204
+ while (i < src.length && src[i] !== '\n')
205
+ i++; // to the end of the member's own line
206
+ if (i < src.length && src[i] === '\n')
207
+ i++; // past the line terminator
208
+ return src.slice(0, i) + text + src.slice(i);
209
+ }
142
210
  function _find_member(cls, pred) {
143
211
  return cls.members.find(pred);
144
212
  }
@@ -178,13 +246,25 @@ function set_field(src, name, expr) {
178
246
  if (existing) {
179
247
  return _apply(src, { start: existing.getStart(sf), end: existing.getEnd(), text: `${name} = ${expr}` });
180
248
  }
181
- return _insert_member(src, ` ${name} = ${expr}\n`);
249
+ // New variable: keep all instance variables grouped at the TOP of the class, in add-order, so
250
+ // every event below can reference them — and a later variable's initializer can safely read an
251
+ // earlier one. Insert after the last existing variable; if none, before the first member.
252
+ const stub = ` ${name} = ${expr}\n`;
253
+ const vars = cls.members.filter(m => ts.isPropertyDeclaration(m) && !_is_static(m));
254
+ if (vars.length)
255
+ return _insert_after(src, vars[vars.length - 1].getEnd(), stub);
256
+ if (cls.members.length)
257
+ return _insert_before(src, cls.members[0].getStart(sf), stub);
258
+ return _insert_member(src, stub);
182
259
  }
183
260
  /** Removes an instance variable field. */
184
261
  function remove_field(src, name) {
185
262
  return _remove_member(src, m => ts.isPropertyDeclaration(m) && !_is_static(m) && _name_of(m) === name);
186
263
  }
187
- /** Adds an `on_*` event method stub if it is not already present. */
264
+ /**
265
+ * Adds an `on_*` event method stub if it is not already present, inserted in canonical event
266
+ * order (before the first existing event method that comes later in EVENT_ORDER).
267
+ */
188
268
  function add_method(src, method, params = '', body = '') {
189
269
  const sf = _source(src);
190
270
  const cls = _find_class(sf);
@@ -193,6 +273,17 @@ function add_method(src, method, params = '', body = '') {
193
273
  if (_find_member(cls, m => ts.isMethodDeclaration(m) && _name_of(m) === method))
194
274
  return src;
195
275
  const stub = ` ${method}(${params}): void {\n${body ? ' ' + body + '\n' : ''} }\n`;
276
+ const order = exports.EVENT_ORDER.indexOf(method);
277
+ if (order >= 0) {
278
+ const after = cls.members.find(m => {
279
+ if (!ts.isMethodDeclaration(m))
280
+ return false;
281
+ const n = _name_of(m);
282
+ return !!n && exports.EVENT_ORDER.indexOf(n) > order;
283
+ });
284
+ if (after)
285
+ return _insert_before(src, after.getStart(sf), stub);
286
+ }
196
287
  return _insert_member(src, stub);
197
288
  }
198
289
  /** Removes an event method by name. */
@@ -222,6 +313,196 @@ function _remove_member(src, pred) {
222
313
  return _apply(src, { start, end, text: '' });
223
314
  }
224
315
  // =========================================================================
316
+ // Event body extract / pack — per-event "virtual file" editing
317
+ // =========================================================================
318
+ //
319
+ // An event is edited as its OWN buffer: get_event_body returns just the method's body,
320
+ // de-indented to column 0, so the editor is a normal standalone file (Ctrl+A, auto-indent,
321
+ // autocomplete — no hidden areas, no live brace-matching). set_event_body packs that buffer
322
+ // back into the method, re-indented. Both are AST-based, so they're robust to braces/strings.
323
+ /** Strips common leading indentation + surrounding blank lines (method body → editable buffer). */
324
+ function _dedent(text) {
325
+ const lines = text.replace(/\r\n?/g, '\n').split('\n');
326
+ while (lines.length && lines[0].trim() === '')
327
+ lines.shift();
328
+ while (lines.length && lines[lines.length - 1].trim() === '')
329
+ lines.pop();
330
+ if (lines.length === 0)
331
+ return '';
332
+ let min = Infinity;
333
+ for (const l of lines) {
334
+ if (l.trim() === '')
335
+ continue;
336
+ const w = /^[ \t]*/.exec(l)[0].length;
337
+ if (w < min)
338
+ min = w;
339
+ }
340
+ if (!isFinite(min))
341
+ min = 0;
342
+ return lines.map(l => (l.trim() === '' ? '' : l.slice(min))).join('\n');
343
+ }
344
+ /** Re-applies an indent prefix to each non-blank line (editable buffer → method body). */
345
+ function _indent(text, indent) {
346
+ return text.replace(/\r\n?/g, '\n').split('\n').map(l => (l.trim() === '' ? '' : indent + l)).join('\n');
347
+ }
348
+ /** Returns the leading whitespace of the line containing `pos`. */
349
+ function _line_indent(src, pos) {
350
+ const line_start = src.lastIndexOf('\n', pos - 1) + 1;
351
+ return /^[ \t]*/.exec(src.slice(line_start, pos))?.[0] ?? '';
352
+ }
353
+ function _find_event_method(cls, method) {
354
+ return cls.members.find(m => ts.isMethodDeclaration(m) && _name_of(m) === method);
355
+ }
356
+ /**
357
+ * Returns one event method's body as an editable, de-indented buffer, or null if absent/bodyless.
358
+ * @param method - The on_* method to extract (e.g. 'on_step')
359
+ */
360
+ function get_event_body(src, method) {
361
+ const sf = _source(src);
362
+ const cls = _find_class(sf);
363
+ if (!cls)
364
+ return null;
365
+ const m = _find_event_method(cls, method);
366
+ if (!m || !m.body)
367
+ return null;
368
+ const inner = src.slice(m.body.getStart(sf) + 1, m.body.getEnd() - 1); // between the braces
369
+ return _dedent(inner);
370
+ }
371
+ /**
372
+ * Replaces an event method's body with `body` (an edited, de-indented buffer), re-indenting it to
373
+ * the method's level. Returns the full updated source; a no-op if the method is absent.
374
+ * @param method - The on_* method to update
375
+ * @param body - The new body (column-0 / de-indented, as edited)
376
+ */
377
+ function set_event_body(src, method, body) {
378
+ const sf = _source(src);
379
+ const cls = _find_class(sf);
380
+ if (!cls)
381
+ return src;
382
+ const m = _find_event_method(cls, method);
383
+ if (!m || !m.body)
384
+ return src;
385
+ const open = m.body.getStart(sf); // position of '{'
386
+ const close = m.body.getEnd() - 1; // position of '}'
387
+ const method_indent = _line_indent(src, m.getStart(sf));
388
+ const dedented = _dedent(body);
389
+ const inner = dedented === '' ? '' : _indent(dedented, method_indent + ' ');
390
+ const block = '{\n' + (inner ? inner + '\n' : '') + method_indent + '}';
391
+ return src.slice(0, open) + block + src.slice(close + 1);
392
+ }
393
+ // =========================================================================
394
+ // Normalize — variables-first + canonical event order + indentation (full class view)
395
+ // =========================================================================
396
+ /**
397
+ * Re-indents code by bracket/brace/paren depth (leading whitespace only — never touches content or
398
+ * spacing). Strings and comments are skipped. Good for typical game code; deeply nested `({`-style
399
+ * continuations may be off by a level (cosmetic, not corrupting).
400
+ */
401
+ function _reindent(code, unit = ' ') {
402
+ const lines = code.replace(/\r\n?/g, '\n').split('\n');
403
+ const out = [];
404
+ let depth = 0;
405
+ let in_block = false; // inside a /* … */ comment spanning lines
406
+ for (const raw of lines) {
407
+ const line = raw.trim();
408
+ if (line === '') {
409
+ out.push('');
410
+ continue;
411
+ }
412
+ let delta = 0, first_is_closer = false, started = false;
413
+ let i = 0;
414
+ let line_block = in_block;
415
+ while (i < line.length) {
416
+ if (line_block) {
417
+ if (line[i] === '*' && line[i + 1] === '/') {
418
+ line_block = false;
419
+ i += 2;
420
+ continue;
421
+ }
422
+ i++;
423
+ continue;
424
+ }
425
+ const c = line[i], n = line[i + 1];
426
+ if (c === '/' && n === '/')
427
+ break;
428
+ if (c === '/' && n === '*') {
429
+ line_block = true;
430
+ i += 2;
431
+ continue;
432
+ }
433
+ if (c === '"' || c === "'" || c === '`') {
434
+ const q = c;
435
+ i++;
436
+ while (i < line.length && line[i] !== q) {
437
+ if (line[i] === '\\')
438
+ i++;
439
+ i++;
440
+ }
441
+ i++;
442
+ continue;
443
+ }
444
+ const opener = c === '{' || c === '(' || c === '[';
445
+ const closer = c === '}' || c === ')' || c === ']';
446
+ if (opener)
447
+ delta++;
448
+ else if (closer)
449
+ delta--;
450
+ if (!started && (opener || closer || /\S/.test(c))) {
451
+ started = true;
452
+ if (closer)
453
+ first_is_closer = true;
454
+ }
455
+ i++;
456
+ }
457
+ in_block = line_block;
458
+ const this_depth = Math.max(0, depth - (first_is_closer ? 1 : 0));
459
+ out.push(unit.repeat(this_depth) + line);
460
+ depth = Math.max(0, depth + delta);
461
+ }
462
+ return out.join('\n');
463
+ }
464
+ /**
465
+ * Reorders all class members into canonical groups, preserving each member's attached comments:
466
+ * 1. instance variables — in their existing relative order (field-init order can matter; one
467
+ * field's initializer may read another), so they're grouped but never shuffled among themselves;
468
+ * 2. `on_*` event methods — in canonical EVENT_ORDER;
469
+ * 3. everything else (statics, helper methods) — in place.
470
+ * Variables-before-events is the whole point: every event can reference every variable. A no-op if
471
+ * the order is already canonical.
472
+ */
473
+ function _reorder_members(src) {
474
+ const sf = _source(src);
475
+ const cls = _find_class(sf);
476
+ if (!cls || cls.members.length <= 1)
477
+ return src;
478
+ const members = cls.members;
479
+ const event_idx = (m) => {
480
+ const n = _name_of(m);
481
+ return n && ts.isMethodDeclaration(m) ? exports.EVENT_ORDER.indexOf(n) : -1;
482
+ };
483
+ const is_var = (m) => ts.isPropertyDeclaration(m) && !_is_static(m);
484
+ const vars = members.filter(is_var);
485
+ const events = members.filter(m => event_idx(m) >= 0).sort((a, b) => event_idx(a) - event_idx(b));
486
+ const others = members.filter(m => !is_var(m) && event_idx(m) < 0);
487
+ const ordered = [...vars, ...events, ...others];
488
+ if (ordered.every((m, i) => m === members[i]))
489
+ return src; // already canonical
490
+ // Rebuild the class body from each member's full text (getFullStart → getEnd includes the leading
491
+ // trivia — newlines/indent/comments — so comments travel with their member). The class header up
492
+ // to `{` and the closing `}` + trailing trivia are preserved verbatim.
493
+ const chunk = (m) => src.slice(m.getFullStart(), m.getEnd());
494
+ const prefix = src.slice(0, members[0].getFullStart());
495
+ const suffix = src.slice(members[members.length - 1].getEnd());
496
+ return prefix + ordered.map(chunk).join('') + suffix;
497
+ }
498
+ /**
499
+ * Normalizes a class-per-object file for display in the full code view: variables hoisted above all
500
+ * events, canonical event order, then consistent indentation. Unchanged if there's no class.
501
+ */
502
+ function normalize_object(src) {
503
+ return _reindent(_reorder_members(src));
504
+ }
505
+ // =========================================================================
225
506
  // Scaffolding
226
507
  // =========================================================================
227
508
  /** Generates a minimal class-file source for a new object. Imports are auto-managed (none needed). */
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Starter templates for the New Project flow.
3
+ *
4
+ * Each template is a real, ready-to-build project folder bundled under `../templates/<id>/`
5
+ * (sprites and all). Creating a project from a template is just a recursive copy of that folder
6
+ * into the destination — no codegen — so the templates stay editable/diffable in the repo and a
7
+ * dogfooded example game *is* the template. Pure Node (fs/path), reusable from the IDE or CLI.
8
+ */
9
+ export interface template_info {
10
+ id: string;
11
+ label: string;
12
+ description: string;
13
+ display_color: string;
14
+ }
15
+ /** Returns the available starter templates — only those whose folder is actually installed. */
16
+ export declare function list_templates(): template_info[];
17
+ /**
18
+ * Materializes a starter template into `dest_folder` by recursively copying its bundled project
19
+ * folder, then (optionally) setting the display name in project.json.
20
+ * @param template_id - 'empty' | 'platformer' | 'topdown'
21
+ * @param dest_folder - Absolute path of the new project folder (created if missing)
22
+ * @param name - Optional display name to write into project.json
23
+ */
24
+ export declare function create_from_template(template_id: string, dest_folder: string, name?: string): Promise<void>;