@silkweaver/build 1.0.0 → 1.2.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.d.ts +10 -0
- package/dist/build.js +140 -14
- 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 +114 -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/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>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Starter templates for the New Project flow.
|
|
4
|
+
*
|
|
5
|
+
* Each template is a real, ready-to-build project folder bundled under `../templates/<id>/`
|
|
6
|
+
* (sprites and all). Creating a project from a template is just a recursive copy of that folder
|
|
7
|
+
* into the destination — no codegen — so the templates stay editable/diffable in the repo and a
|
|
8
|
+
* dogfooded example game *is* the template. Pure Node (fs/path), reusable from the IDE or CLI.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.list_templates = list_templates;
|
|
45
|
+
exports.create_from_template = create_from_template;
|
|
46
|
+
const fs = __importStar(require("node:fs"));
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
48
|
+
const build_js_1 = require("./build.js");
|
|
49
|
+
/** Ordered registry of the bundled starter templates (folders live under ../templates/<id>). */
|
|
50
|
+
const TEMPLATE_REGISTRY = [
|
|
51
|
+
{ id: 'empty', label: 'Empty', description: 'A blank project with a single empty room.' },
|
|
52
|
+
{ id: 'platformer', label: 'Platformer', description: 'A/D to move, Space to jump — gravity, solid platforms, parent-based collision.' },
|
|
53
|
+
{ id: 'topdown', label: 'Top-down', description: 'WASD movement with parent-based wall collision (place_meeting + a _col parent).' },
|
|
54
|
+
];
|
|
55
|
+
/** Absolute path to the bundled templates directory (sibling of dist/). */
|
|
56
|
+
function templates_dir() {
|
|
57
|
+
return path.join(__dirname, '..', 'templates');
|
|
58
|
+
}
|
|
59
|
+
/** Names never copied into a new project (build artifacts / VCS / deps), as a defensive guard. */
|
|
60
|
+
const COPY_DENYLIST = new Set(['_entry.ts', '_engine_globals.ts', 'node_modules', '.git', 'exports', 'game.js', '.engine']);
|
|
61
|
+
/** Returns the available starter templates — only those whose folder is actually installed. */
|
|
62
|
+
function list_templates() {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const t of TEMPLATE_REGISTRY) {
|
|
65
|
+
let display_color = '#1a1a2e';
|
|
66
|
+
try {
|
|
67
|
+
const j = JSON.parse(fs.readFileSync(path.join(templates_dir(), t.id, 'project.json'), 'utf8'));
|
|
68
|
+
if (typeof j?.settings?.displayColor === 'string')
|
|
69
|
+
display_color = j.settings.displayColor;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
continue;
|
|
73
|
+
} // folder missing/unreadable → omit it
|
|
74
|
+
out.push({ id: t.id, label: t.label, description: t.description, display_color });
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Materializes a starter template into `dest_folder` by recursively copying its bundled project
|
|
80
|
+
* folder, then (optionally) setting the display name in project.json.
|
|
81
|
+
* @param template_id - 'empty' | 'platformer' | 'topdown'
|
|
82
|
+
* @param dest_folder - Absolute path of the new project folder (created if missing)
|
|
83
|
+
* @param name - Optional display name to write into project.json
|
|
84
|
+
*/
|
|
85
|
+
async function create_from_template(template_id, dest_folder, name) {
|
|
86
|
+
if (!TEMPLATE_REGISTRY.some(t => t.id === template_id))
|
|
87
|
+
throw new Error(`Unknown template: ${template_id}`);
|
|
88
|
+
const src = path.join(templates_dir(), template_id);
|
|
89
|
+
if (!fs.existsSync(path.join(src, 'project.json')))
|
|
90
|
+
throw new Error(`Template '${template_id}' is not installed`);
|
|
91
|
+
if (fs.existsSync(path.join(dest_folder, 'project.json')))
|
|
92
|
+
throw new Error('A project already exists in that folder');
|
|
93
|
+
await fs.promises.mkdir(dest_folder, { recursive: true });
|
|
94
|
+
await fs.promises.cp(src, dest_folder, {
|
|
95
|
+
recursive: true,
|
|
96
|
+
filter: (s) => !COPY_DENYLIST.has(path.basename(s)),
|
|
97
|
+
});
|
|
98
|
+
if (name && name.trim()) {
|
|
99
|
+
const proj_path = path.join(dest_folder, 'project.json');
|
|
100
|
+
try {
|
|
101
|
+
const j = JSON.parse(await fs.promises.readFile(proj_path, 'utf8'));
|
|
102
|
+
j.name = name.trim();
|
|
103
|
+
await fs.promises.writeFile(proj_path, JSON.stringify(j, null, 2) + '\n', 'utf8');
|
|
104
|
+
}
|
|
105
|
+
catch { /* keep the template's bundled name */ }
|
|
106
|
+
}
|
|
107
|
+
// Vendor the current engine into the new project so it's pinned to this version (a later IDE
|
|
108
|
+
// update won't change it). Best-effort: if it fails, the project still builds against the
|
|
109
|
+
// toolchain engine via the resolve_engine fallback.
|
|
110
|
+
try {
|
|
111
|
+
await (0, build_js_1.vendor_engine)(dest_folder);
|
|
112
|
+
}
|
|
113
|
+
catch { /* falls back to the toolchain engine */ }
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silkweaver/build",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Silkweaver toolchain — compiles a project folder into a runnable game (HTML5 / desktop executable). Usable from a CLI or the IDE.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"license": "GPL-3.0",
|
|
@@ -23,14 +23,15 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist",
|
|
26
|
-
"assets"
|
|
26
|
+
"assets",
|
|
27
|
+
"templates"
|
|
27
28
|
],
|
|
28
29
|
"scripts": {
|
|
29
30
|
"build": "tsc -p ."
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"@silkweaver/engine": "1.0.0",
|
|
33
|
-
"@silkweaver/project": "1.0.0",
|
|
33
|
+
"@silkweaver/engine": "^1.0.0",
|
|
34
|
+
"@silkweaver/project": "^1.0.0",
|
|
34
35
|
"@electron/packager": "^19.0.5",
|
|
35
36
|
"esbuild": "^0.27.2",
|
|
36
37
|
"typescript": "^5.9.3"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Empty",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"engineVersion": "1.0.0",
|
|
5
|
+
"settings": {
|
|
6
|
+
"roomSpeed": 60,
|
|
7
|
+
"windowWidth": 640,
|
|
8
|
+
"windowHeight": 480,
|
|
9
|
+
"startRoom": "room_main",
|
|
10
|
+
"displayColor": "#1a1a2e"
|
|
11
|
+
},
|
|
12
|
+
"resources": {
|
|
13
|
+
"sprites": {},
|
|
14
|
+
"sounds": {},
|
|
15
|
+
"backgrounds": {},
|
|
16
|
+
"paths": {},
|
|
17
|
+
"scripts": {},
|
|
18
|
+
"fonts": {},
|
|
19
|
+
"timelines": {},
|
|
20
|
+
"objects": {},
|
|
21
|
+
"rooms": {
|
|
22
|
+
"room_main": {
|
|
23
|
+
"name": "room_main"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"width": 640,
|
|
3
|
+
"height": 480,
|
|
4
|
+
"room_speed": 60,
|
|
5
|
+
"persistent": false,
|
|
6
|
+
"creation_code": "",
|
|
7
|
+
"instances": [],
|
|
8
|
+
"backgrounds": [],
|
|
9
|
+
"views": [],
|
|
10
|
+
"tiles": [],
|
|
11
|
+
"bg_color": "#1a1a2e",
|
|
12
|
+
"bg_show_color": true,
|
|
13
|
+
"physics_world": false,
|
|
14
|
+
"physics_gravity_x": 0,
|
|
15
|
+
"physics_gravity_y": 10
|
|
16
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export class obj_player extends gm_object {
|
|
2
|
+
spd = 5;
|
|
3
|
+
jump_force = 15;
|
|
4
|
+
weight = 1;
|
|
5
|
+
|
|
6
|
+
on_create(): void {
|
|
7
|
+
this.grounded = false;
|
|
8
|
+
this.vertical_acc = 0.0;
|
|
9
|
+
this.vertical_vel = 0.0;
|
|
10
|
+
this.horizontal_vel = 0.0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
on_step(): void {
|
|
14
|
+
// controlls
|
|
15
|
+
let key_left = keyboard_check(ord("A"));
|
|
16
|
+
let key_right = keyboard_check(ord("D"));
|
|
17
|
+
let key_jump = keyboard_check(vk_space);
|
|
18
|
+
|
|
19
|
+
// ------------------------------
|
|
20
|
+
// left to right
|
|
21
|
+
// ------------------------------
|
|
22
|
+
|
|
23
|
+
// motion
|
|
24
|
+
let dir = 0;
|
|
25
|
+
if (key_left) {
|
|
26
|
+
dir = -1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (key_right) {
|
|
30
|
+
dir = 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// movenemt
|
|
34
|
+
this.horizontal_vel = dir * this.spd;
|
|
35
|
+
if (!this.place_meeting(this.x + this.horizontal_vel, this.y, _col)) {
|
|
36
|
+
this.x += this.horizontal_vel;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ------------------------------
|
|
40
|
+
// jump and gravity
|
|
41
|
+
// ------------------------------
|
|
42
|
+
|
|
43
|
+
// grounding
|
|
44
|
+
if (this.place_meeting(this.x, this.y + 1, _col)) {
|
|
45
|
+
this.grounded = true;
|
|
46
|
+
this.vertical_vel = 0;
|
|
47
|
+
this.vertical_acc = 0;
|
|
48
|
+
} else {
|
|
49
|
+
this.grounded = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// vertical acceleration (gravity pulls down while airborne)
|
|
53
|
+
if (!this.grounded) {
|
|
54
|
+
this.vertical_acc = -this.weight;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// jump
|
|
58
|
+
if (key_jump && this.grounded) {
|
|
59
|
+
this.vertical_acc = this.jump_force;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// apply acceleration, then move — snapping flush against anything in the way
|
|
63
|
+
this.vertical_vel += this.vertical_acc;
|
|
64
|
+
if (this.place_meeting(this.x, this.y - this.vertical_vel, _col)) {
|
|
65
|
+
while (!this.place_meeting(this.x, this.y - sign(this.vertical_vel), _col)) {
|
|
66
|
+
this.y -= sign(this.vertical_vel);
|
|
67
|
+
}
|
|
68
|
+
this.vertical_vel = 0;
|
|
69
|
+
}
|
|
70
|
+
this.y -= this.vertical_vel;
|
|
71
|
+
}
|
|
72
|
+
static sprite = 'spr_player'
|
|
73
|
+
}
|