@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 +71 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/object_format.d.ts +31 -1
- package/dist/object_format.js +283 -2
- package/dist/templates.d.ts +24 -0
- package/dist/templates.js +106 -0
- package/package.json +5 -4
- package/templates/empty/project.json +27 -0
- package/templates/empty/rooms/room_main/room.json +16 -0
- package/templates/platformer/objects/_col.ts +3 -0
- package/templates/platformer/objects/obj_platform.ts +10 -0
- package/templates/platformer/objects/obj_player.ts +73 -0
- package/templates/platformer/project.json +44 -0
- package/templates/platformer/rooms/room_main/room.json +327 -0
- package/templates/platformer/sprites/spr_platform/0.png +0 -0
- package/templates/platformer/sprites/spr_platform/meta.json +17 -0
- package/templates/platformer/sprites/spr_player/0.png +0 -0
- package/templates/platformer/sprites/spr_player/meta.json +17 -0
- package/templates/topdown/objects/_col.ts +2 -0
- package/templates/topdown/objects/obj_player.ts +45 -0
- package/templates/topdown/objects/obj_wall.ts +6 -0
- package/templates/topdown/project.json +44 -0
- package/templates/topdown/rooms/room_main/room.json +727 -0
- package/templates/topdown/sprites/spr_player/0.png +0 -0
- package/templates/topdown/sprites/spr_player/meta.json +17 -0
- package/templates/topdown/sprites/spr_wall/0.png +0 -0
- package/templates/topdown/sprites/spr_wall/meta.json +17 -0
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|
package/dist/object_format.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/object_format.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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>;
|