@auraindustry/aurajs 0.0.2 → 0.0.4
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/README.md +52 -0
- package/package.json +13 -1
- package/src/build-contract.mjs +1359 -2
- package/src/cli.mjs +1584 -13
- package/src/conformance.mjs +1013 -86
- package/src/game-state-runtime.mjs +1067 -0
- package/src/headless-test.mjs +525 -10
- package/src/host-binary.mjs +33 -10
- package/src/react/aura-game.mjs +310 -0
- package/src/react/index.mjs +1 -0
- package/src/scaffold.mjs +267 -13
- package/src/web-api.mjs +454 -0
- package/templates/create/2d/aura.config.json +28 -0
- package/templates/create/2d/src/main.js +196 -0
- package/templates/create/3d/aura.config.json +28 -0
- package/templates/create/3d/src/main.js +306 -0
- package/templates/create/blank/aura.config.json +28 -0
- package/templates/create/blank/src/main.js +28 -0
- package/templates/create/shared/src/starter-utils/core.js +114 -0
- package/templates/create/shared/src/starter-utils/enemy-archetypes-2d.js +68 -0
- package/templates/create/shared/src/starter-utils/index.js +6 -0
- package/templates/create/shared/src/starter-utils/platformer-3d.js +101 -0
- package/templates/create/shared/src/starter-utils/wave-director.js +101 -0
- package/templates/skills/aurajs/SKILL.md +40 -0
- package/templates/skills/aurajs/api-contract-3d.md +7 -0
- package/templates/skills/aurajs/api-contract.md +7 -0
- package/templates/starter/src/main.js +48 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { createAuraGame } from '../web-api.mjs';
|
|
4
|
+
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
return value != null && typeof value === 'object' && Object.prototype.toString.call(value) === '[object Object]';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function stableSerialize(value) {
|
|
10
|
+
if (value == null) return 'null';
|
|
11
|
+
if (typeof value === 'boolean' || typeof value === 'number') return JSON.stringify(value);
|
|
12
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
return '[' + value.map((entry) => stableSerialize(entry)).join(',') + ']';
|
|
15
|
+
}
|
|
16
|
+
if (isPlainObject(value)) {
|
|
17
|
+
const keys = Object.keys(value).sort();
|
|
18
|
+
const fields = [];
|
|
19
|
+
for (const key of keys) {
|
|
20
|
+
fields.push(JSON.stringify(key) + ':' + stableSerialize(value[key]));
|
|
21
|
+
}
|
|
22
|
+
return '{' + fields.join(',') + '}';
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify(String(value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createReactError(reasonCode, message, retryable, details) {
|
|
28
|
+
const error = new Error(message);
|
|
29
|
+
error.ok = false;
|
|
30
|
+
error.reasonCode = reasonCode;
|
|
31
|
+
error.layer = 'react';
|
|
32
|
+
error.retryable = retryable === true;
|
|
33
|
+
error.details = isPlainObject(details) ? { ...details } : {};
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeReasonCodedError(error, fallbackReasonCode, fallbackMessage, fallbackRetryable) {
|
|
38
|
+
if (error && typeof error === 'object' && typeof error.reasonCode === 'string') {
|
|
39
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
return createReactError(
|
|
42
|
+
fallbackReasonCode,
|
|
43
|
+
fallbackMessage,
|
|
44
|
+
fallbackRetryable,
|
|
45
|
+
{ cause: String(error && error.message ? error.message : error) },
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stableErrorKey(error) {
|
|
50
|
+
if (!error || typeof error !== 'object') return null;
|
|
51
|
+
return stableSerialize({
|
|
52
|
+
reasonCode: typeof error.reasonCode === 'string' ? error.reasonCode : null,
|
|
53
|
+
layer: typeof error.layer === 'string' ? error.layer : null,
|
|
54
|
+
retryable: error.retryable === true,
|
|
55
|
+
details: isPlainObject(error.details) ? error.details : {},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeSource(source) {
|
|
60
|
+
if (source == null) return '.';
|
|
61
|
+
if (typeof source !== 'string' || source.trim().length === 0) {
|
|
62
|
+
throw createReactError(
|
|
63
|
+
'react_auragame_invalid_source',
|
|
64
|
+
'AuraGame prop source must be a non-empty string.',
|
|
65
|
+
false,
|
|
66
|
+
{},
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return source.trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeConfig(config) {
|
|
73
|
+
if (config == null) return {};
|
|
74
|
+
if (!isPlainObject(config)) {
|
|
75
|
+
throw createReactError(
|
|
76
|
+
'react_auragame_invalid_config',
|
|
77
|
+
'AuraGame prop config must be a plain object.',
|
|
78
|
+
false,
|
|
79
|
+
{},
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return { ...config };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function safeUnmount(game) {
|
|
86
|
+
if (!game || typeof game.unmount !== 'function') return;
|
|
87
|
+
try {
|
|
88
|
+
await game.unmount();
|
|
89
|
+
} catch (_) {
|
|
90
|
+
// Cleanup should not throw through React unmount paths.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function AuraGame(props = {}) {
|
|
95
|
+
const {
|
|
96
|
+
source = '.',
|
|
97
|
+
config = null,
|
|
98
|
+
restartKey = null,
|
|
99
|
+
loader = null,
|
|
100
|
+
className,
|
|
101
|
+
style,
|
|
102
|
+
mountClassName,
|
|
103
|
+
mountStyle,
|
|
104
|
+
onReady,
|
|
105
|
+
onPhaseChange,
|
|
106
|
+
onLifecycle,
|
|
107
|
+
onLoadingChange,
|
|
108
|
+
onError,
|
|
109
|
+
renderLoading,
|
|
110
|
+
renderError,
|
|
111
|
+
} = props;
|
|
112
|
+
|
|
113
|
+
const mountNodeRef = useRef(null);
|
|
114
|
+
const gameRef = useRef(null);
|
|
115
|
+
const callbacksRef = useRef({
|
|
116
|
+
onReady: null,
|
|
117
|
+
onPhaseChange: null,
|
|
118
|
+
onLifecycle: null,
|
|
119
|
+
onLoadingChange: null,
|
|
120
|
+
onError: null,
|
|
121
|
+
});
|
|
122
|
+
callbacksRef.current = {
|
|
123
|
+
onReady: typeof onReady === 'function' ? onReady : null,
|
|
124
|
+
onPhaseChange: typeof onPhaseChange === 'function' ? onPhaseChange : null,
|
|
125
|
+
onLifecycle: typeof onLifecycle === 'function' ? onLifecycle : null,
|
|
126
|
+
onLoadingChange: typeof onLoadingChange === 'function' ? onLoadingChange : null,
|
|
127
|
+
onError: typeof onError === 'function' ? onError : null,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const [phase, setPhase] = useState('idle');
|
|
131
|
+
const [loading, setLoading] = useState(false);
|
|
132
|
+
const [error, setError] = useState(null);
|
|
133
|
+
const loadingRef = useRef(false);
|
|
134
|
+
const lastErrorKeyRef = useRef(null);
|
|
135
|
+
|
|
136
|
+
const configSignature = useMemo(() => stableSerialize(config), [config]);
|
|
137
|
+
const restartSignature = useMemo(
|
|
138
|
+
() => stableSerialize({ source, config: configSignature, restartKey }),
|
|
139
|
+
[source, configSignature, restartKey],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
function setLoadingState(nextLoading) {
|
|
143
|
+
const next = nextLoading === true;
|
|
144
|
+
if (loadingRef.current === next) return;
|
|
145
|
+
loadingRef.current = next;
|
|
146
|
+
setLoading(next);
|
|
147
|
+
if (callbacksRef.current.onLoadingChange) {
|
|
148
|
+
callbacksRef.current.onLoadingChange(next);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function emitError(nextError, stateSnapshot) {
|
|
153
|
+
const normalized = normalizeReasonCodedError(
|
|
154
|
+
nextError,
|
|
155
|
+
'react_auragame_mount_failed',
|
|
156
|
+
'Failed to mount AuraGame.',
|
|
157
|
+
true,
|
|
158
|
+
);
|
|
159
|
+
const key = stableErrorKey(normalized);
|
|
160
|
+
if (key === lastErrorKeyRef.current) {
|
|
161
|
+
return normalized;
|
|
162
|
+
}
|
|
163
|
+
lastErrorKeyRef.current = key;
|
|
164
|
+
setError(normalized);
|
|
165
|
+
if (callbacksRef.current.onError) {
|
|
166
|
+
callbacksRef.current.onError(normalized, stateSnapshot || null);
|
|
167
|
+
}
|
|
168
|
+
return normalized;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
let cancelled = false;
|
|
173
|
+
const mountNode = mountNodeRef.current;
|
|
174
|
+
if (!mountNode) {
|
|
175
|
+
const mountError = createReactError(
|
|
176
|
+
'react_auragame_mount_node_missing',
|
|
177
|
+
'AuraGame mount node is unavailable.',
|
|
178
|
+
false,
|
|
179
|
+
{},
|
|
180
|
+
);
|
|
181
|
+
setPhase('error');
|
|
182
|
+
setLoadingState(false);
|
|
183
|
+
emitError(mountError, { phase: 'error', reasonCode: mountError.reasonCode });
|
|
184
|
+
return () => {};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let normalizedSource = '.';
|
|
188
|
+
let normalizedConfig = {};
|
|
189
|
+
try {
|
|
190
|
+
normalizedSource = normalizeSource(source);
|
|
191
|
+
normalizedConfig = normalizeConfig(config);
|
|
192
|
+
} catch (validationError) {
|
|
193
|
+
setPhase('error');
|
|
194
|
+
setLoadingState(false);
|
|
195
|
+
emitError(validationError, { phase: 'error', reasonCode: validationError.reasonCode });
|
|
196
|
+
return () => {};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
setPhase('idle');
|
|
200
|
+
setError(null);
|
|
201
|
+
lastErrorKeyRef.current = null;
|
|
202
|
+
setLoadingState(true);
|
|
203
|
+
|
|
204
|
+
const game = createAuraGame({
|
|
205
|
+
loader,
|
|
206
|
+
onPhaseChange: (nextPhase, stateSnapshot) => {
|
|
207
|
+
if (cancelled) return;
|
|
208
|
+
setPhase(typeof nextPhase === 'string' ? nextPhase : 'idle');
|
|
209
|
+
if (callbacksRef.current.onPhaseChange) {
|
|
210
|
+
callbacksRef.current.onPhaseChange(nextPhase, stateSnapshot);
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
onLifecycle: (lifecycle, stateSnapshot) => {
|
|
214
|
+
if (cancelled) return;
|
|
215
|
+
if (callbacksRef.current.onLifecycle) {
|
|
216
|
+
callbacksRef.current.onLifecycle(lifecycle, stateSnapshot);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
onError: (nextError, stateSnapshot) => {
|
|
220
|
+
if (cancelled) return;
|
|
221
|
+
setPhase('error');
|
|
222
|
+
emitError(nextError, stateSnapshot);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
gameRef.current = game;
|
|
226
|
+
|
|
227
|
+
async function start() {
|
|
228
|
+
try {
|
|
229
|
+
game.setConfig({
|
|
230
|
+
...normalizedConfig,
|
|
231
|
+
rootUrl: normalizedSource,
|
|
232
|
+
mount: mountNode,
|
|
233
|
+
});
|
|
234
|
+
const mountedState = await game.mount(mountNode, {
|
|
235
|
+
...normalizedConfig,
|
|
236
|
+
rootUrl: normalizedSource,
|
|
237
|
+
mount: mountNode,
|
|
238
|
+
});
|
|
239
|
+
if (cancelled) {
|
|
240
|
+
await safeUnmount(game);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
setPhase(typeof mountedState.phase === 'string' ? mountedState.phase : 'mounted');
|
|
244
|
+
setError(null);
|
|
245
|
+
setLoadingState(false);
|
|
246
|
+
if (callbacksRef.current.onReady) {
|
|
247
|
+
callbacksRef.current.onReady(mountedState);
|
|
248
|
+
}
|
|
249
|
+
} catch (mountError) {
|
|
250
|
+
if (cancelled) return;
|
|
251
|
+
const normalized = normalizeReasonCodedError(
|
|
252
|
+
mountError,
|
|
253
|
+
'react_auragame_mount_failed',
|
|
254
|
+
'Failed to mount AuraGame.',
|
|
255
|
+
true,
|
|
256
|
+
);
|
|
257
|
+
setPhase('error');
|
|
258
|
+
setLoadingState(false);
|
|
259
|
+
emitError(normalized, { phase: 'error', reasonCode: normalized.reasonCode });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
void start();
|
|
264
|
+
|
|
265
|
+
return () => {
|
|
266
|
+
cancelled = true;
|
|
267
|
+
if (gameRef.current === game) {
|
|
268
|
+
gameRef.current = null;
|
|
269
|
+
}
|
|
270
|
+
void safeUnmount(game);
|
|
271
|
+
};
|
|
272
|
+
}, [loader, restartSignature]);
|
|
273
|
+
|
|
274
|
+
const children = [
|
|
275
|
+
React.createElement('div', {
|
|
276
|
+
key: 'mount',
|
|
277
|
+
ref: mountNodeRef,
|
|
278
|
+
className: mountClassName,
|
|
279
|
+
style: mountStyle,
|
|
280
|
+
'data-aura-game-mount': 'true',
|
|
281
|
+
}),
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
if (loading && typeof renderLoading === 'function') {
|
|
285
|
+
children.push(React.createElement(React.Fragment, { key: 'loading' }, renderLoading({
|
|
286
|
+
phase,
|
|
287
|
+
loading,
|
|
288
|
+
error,
|
|
289
|
+
})));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (error && typeof renderError === 'function') {
|
|
293
|
+
children.push(React.createElement(React.Fragment, { key: 'error' }, renderError(error, {
|
|
294
|
+
phase,
|
|
295
|
+
loading,
|
|
296
|
+
error,
|
|
297
|
+
})));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return React.createElement('div', {
|
|
301
|
+
className,
|
|
302
|
+
style,
|
|
303
|
+
'data-aura-game': 'true',
|
|
304
|
+
'data-aura-phase': phase,
|
|
305
|
+
'data-aura-loading': loading ? 'true' : 'false',
|
|
306
|
+
'data-aura-error-code': error && typeof error.reasonCode === 'string' ? error.reasonCode : '',
|
|
307
|
+
}, children);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export default AuraGame;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AuraGame, default } from './aura-game.mjs';
|
package/src/scaffold.mjs
CHANGED
|
@@ -2,36 +2,290 @@ import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, statSy
|
|
|
2
2
|
import { resolve, join, dirname } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const CLI_SRC_DIR = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
const LEGACY_STARTER_TEMPLATE_DIR = resolve(CLI_SRC_DIR, '../templates/starter');
|
|
8
|
+
|
|
9
|
+
const CREATE_TEMPLATE_DIRS = {
|
|
10
|
+
'2d-shooter': resolve(CLI_SRC_DIR, '../templates/create/2d'),
|
|
11
|
+
'3d-platformer': resolve(CLI_SRC_DIR, '../templates/create/3d'),
|
|
12
|
+
blank: resolve(CLI_SRC_DIR, '../templates/create/blank'),
|
|
13
|
+
};
|
|
14
|
+
const CREATE_SHARED_TEMPLATE_DIR = resolve(CLI_SRC_DIR, '../templates/create/shared');
|
|
15
|
+
|
|
16
|
+
const CREATE_TEMPLATE_ALIASES = {
|
|
17
|
+
'2d': '2d-shooter',
|
|
18
|
+
shooter: '2d-shooter',
|
|
19
|
+
'2d shooter': '2d-shooter',
|
|
20
|
+
'2d-shooter': '2d-shooter',
|
|
21
|
+
'3d': '3d-platformer',
|
|
22
|
+
platformer: '3d-platformer',
|
|
23
|
+
platformers: '3d-platformer',
|
|
24
|
+
'3d platformer': '3d-platformer',
|
|
25
|
+
'3d platformers': '3d-platformer',
|
|
26
|
+
'3d-platformer': '3d-platformer',
|
|
27
|
+
blank: 'blank',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ROOT_SKILLS_DIR = resolve(CLI_SRC_DIR, '../../../skills');
|
|
31
|
+
const PACKAGED_SKILLS_DIR = resolve(CLI_SRC_DIR, '../templates/skills');
|
|
32
|
+
|
|
33
|
+
const GITIGNORE_TEMPLATE = `# Dependencies\nnode_modules/\n\n# Build output\nbuild/\n.aura/\n\n# Runtime logs\n.logs/\n*.log\nnpm-debug.log*\n\n# Local env\n.env\n.env.*\n`;
|
|
34
|
+
|
|
35
|
+
const PLAY_BIN_TEMPLATE = `#!/usr/bin/env node
|
|
36
|
+
|
|
37
|
+
import { spawn } from 'node:child_process';
|
|
38
|
+
import { existsSync, mkdirSync, createWriteStream } from 'node:fs';
|
|
39
|
+
import { resolve, dirname, join } from 'node:path';
|
|
40
|
+
import { fileURLToPath } from 'node:url';
|
|
41
|
+
|
|
42
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const projectRoot = resolve(__dirname, '..');
|
|
44
|
+
const localAuraCli = resolve(projectRoot, 'node_modules', '@auraindustry', 'aurajs', 'src', 'cli.mjs');
|
|
45
|
+
const runArgs = ['run', '--asset-mode', 'sibling'];
|
|
46
|
+
|
|
47
|
+
const logDir = resolve(projectRoot, '.logs');
|
|
48
|
+
mkdirSync(logDir, { recursive: true });
|
|
49
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
50
|
+
const logPath = join(logDir, \`play-\${stamp}.log\`);
|
|
51
|
+
const logStream = createWriteStream(logPath, { flags: 'a' });
|
|
52
|
+
|
|
53
|
+
const command = existsSync(localAuraCli)
|
|
54
|
+
? {
|
|
55
|
+
cmd: process.execPath,
|
|
56
|
+
args: [localAuraCli, ...runArgs],
|
|
57
|
+
}
|
|
58
|
+
: {
|
|
59
|
+
cmd: 'npm',
|
|
60
|
+
args: ['exec', '--yes', '--package', '@auraindustry/aurajs', '--', 'aura', ...runArgs],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
logStream.write(\`[play] \${new Date().toISOString()} command=\${command.cmd} \${command.args.join(' ')}\\n\`);
|
|
64
|
+
|
|
65
|
+
const child = spawn(command.cmd, command.args, {
|
|
66
|
+
cwd: projectRoot,
|
|
67
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
68
|
+
env: process.env,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function pipeToConsoleAndLog(stream, target) {
|
|
72
|
+
if (!stream) return;
|
|
73
|
+
stream.on('data', (chunk) => {
|
|
74
|
+
target.write(chunk);
|
|
75
|
+
logStream.write(chunk);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pipeToConsoleAndLog(child.stdout, process.stdout);
|
|
80
|
+
pipeToConsoleAndLog(child.stderr, process.stderr);
|
|
81
|
+
|
|
82
|
+
child.on('error', (err) => {
|
|
83
|
+
const message = \`[play:error] \${err.message}\\n\`;
|
|
84
|
+
process.stderr.write(message);
|
|
85
|
+
logStream.write(message);
|
|
86
|
+
logStream.end();
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
child.on('close', (code) => {
|
|
91
|
+
logStream.write(\`\\n[play:exit] code=\${code ?? 1}\\n\`);
|
|
92
|
+
logStream.end(() => {
|
|
93
|
+
process.exit(code ?? 1);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
export function listCreateTemplates() {
|
|
99
|
+
return Object.keys(CREATE_TEMPLATE_DIRS);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeCreateTemplate(template) {
|
|
103
|
+
const normalized = String(template || '').trim().toLowerCase();
|
|
104
|
+
return CREATE_TEMPLATE_ALIASES[normalized] || null;
|
|
105
|
+
}
|
|
9
106
|
|
|
10
107
|
/**
|
|
11
|
-
* Scaffold
|
|
108
|
+
* Scaffold the legacy starter template (`aura init`).
|
|
12
109
|
* @param {string} name - Project name (used as directory name and in config).
|
|
13
110
|
* @param {string} dest - Absolute path to create the project at.
|
|
14
111
|
*/
|
|
15
112
|
export function scaffold(name, dest) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
113
|
+
assertDestinationIsEmpty(name, dest);
|
|
114
|
+
|
|
115
|
+
copyTree(LEGACY_STARTER_TEMPLATE_DIR, dest, { PROJECT_NAME: name });
|
|
116
|
+
mkdirSync(join(dest, 'assets'), { recursive: true });
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
dest,
|
|
120
|
+
files: listFiles(dest),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Scaffold a full npm-ready AuraJS game project (`aura create`).
|
|
126
|
+
* @param {object} options - Scaffolding options.
|
|
127
|
+
* @param {string} options.name - Project directory name (npm package defaults to @aurajs/<name>).
|
|
128
|
+
* @param {string} options.dest - Absolute destination directory.
|
|
129
|
+
* @param {string} [options.template='2d-shooter'] - Template key: 2d-shooter | 3d-platformer | blank.
|
|
130
|
+
* @param {string} [options.version='0.1.0'] - Initial package version.
|
|
131
|
+
* @param {string} [options.license='MIT'] - Initial package license.
|
|
132
|
+
*/
|
|
133
|
+
export function scaffoldGame(options) {
|
|
134
|
+
const {
|
|
135
|
+
name,
|
|
136
|
+
dest,
|
|
137
|
+
template = '2d-shooter',
|
|
138
|
+
version = '0.1.0',
|
|
139
|
+
license = 'MIT',
|
|
140
|
+
} = options;
|
|
141
|
+
|
|
142
|
+
const normalizedTemplate = normalizeCreateTemplate(template);
|
|
143
|
+
const templateDir = CREATE_TEMPLATE_DIRS[normalizedTemplate];
|
|
144
|
+
if (!templateDir || !existsSync(templateDir)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Unknown template "${template}". Expected one of: ${listCreateTemplates().join(', ')} (aliases: 2d, 3d, shooter, platformer, blank).`,
|
|
147
|
+
);
|
|
21
148
|
}
|
|
22
149
|
|
|
23
|
-
|
|
24
|
-
|
|
150
|
+
assertDestinationIsEmpty(name, dest);
|
|
151
|
+
|
|
152
|
+
const binName = sanitizeBinName(name);
|
|
153
|
+
const projectTitle = toDisplayTitle(name);
|
|
154
|
+
const replacements = {
|
|
155
|
+
PROJECT_NAME: name,
|
|
156
|
+
PROJECT_TITLE: projectTitle,
|
|
157
|
+
PROJECT_BIN_NAME: binName,
|
|
158
|
+
PROJECT_VERSION: version,
|
|
159
|
+
PROJECT_LICENSE: license,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
copyTree(templateDir, dest, replacements);
|
|
163
|
+
if (normalizedTemplate !== 'blank' && existsSync(CREATE_SHARED_TEMPLATE_DIR)) {
|
|
164
|
+
copyTree(CREATE_SHARED_TEMPLATE_DIR, dest, replacements);
|
|
165
|
+
}
|
|
25
166
|
|
|
26
|
-
// Ensure assets/ directory exists (even if empty in template).
|
|
27
167
|
mkdirSync(join(dest, 'assets'), { recursive: true });
|
|
168
|
+
mkdirSync(join(dest, 'bin'), { recursive: true });
|
|
169
|
+
mkdirSync(join(dest, '.logs'), { recursive: true });
|
|
170
|
+
writeFileSync(join(dest, '.logs', '.gitkeep'), '', 'utf8');
|
|
171
|
+
|
|
172
|
+
const pkg = buildPackageJson({
|
|
173
|
+
name,
|
|
174
|
+
version,
|
|
175
|
+
license,
|
|
176
|
+
binName,
|
|
177
|
+
template: normalizedTemplate,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
writeFileSync(join(dest, 'package.json'), JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
181
|
+
writeFileSync(join(dest, '.gitignore'), GITIGNORE_TEMPLATE, 'utf8');
|
|
182
|
+
writeFileSync(join(dest, 'bin', 'play.js'), PLAY_BIN_TEMPLATE, { encoding: 'utf8', mode: 0o755 });
|
|
183
|
+
|
|
184
|
+
const skillsSource = resolveSkillsSourceDir();
|
|
185
|
+
if (skillsSource) {
|
|
186
|
+
copyTree(skillsSource, join(dest, 'skills'), replacements);
|
|
187
|
+
} else {
|
|
188
|
+
mkdirSync(join(dest, 'skills'), { recursive: true });
|
|
189
|
+
}
|
|
28
190
|
|
|
29
191
|
return {
|
|
30
192
|
dest,
|
|
193
|
+
template: normalizedTemplate,
|
|
31
194
|
files: listFiles(dest),
|
|
32
195
|
};
|
|
33
196
|
}
|
|
34
197
|
|
|
198
|
+
function sanitizeBinName(name) {
|
|
199
|
+
const normalized = String(name || '').trim();
|
|
200
|
+
const compact = normalized
|
|
201
|
+
.replace(/^@[^/]+\//, '')
|
|
202
|
+
.replace(/\s+/g, '-')
|
|
203
|
+
.replace(/[^a-zA-Z0-9._-]/g, '-')
|
|
204
|
+
.replace(/-+/g, '-')
|
|
205
|
+
.replace(/^[-._]+|[-._]+$/g, '')
|
|
206
|
+
.toLowerCase();
|
|
207
|
+
|
|
208
|
+
if (!compact) {
|
|
209
|
+
throw new Error(`Unable to derive executable name from project name "${name}".`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return compact;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function sanitizePackageSlug(name) {
|
|
216
|
+
const normalized = String(name || '').trim();
|
|
217
|
+
const compact = normalized
|
|
218
|
+
.replace(/^@[^/]+\//, '')
|
|
219
|
+
.replace(/\s+/g, '-')
|
|
220
|
+
.replace(/[^a-zA-Z0-9._-]/g, '-')
|
|
221
|
+
.replace(/-+/g, '-')
|
|
222
|
+
.replace(/^[-._]+|[-._]+$/g, '')
|
|
223
|
+
.toLowerCase();
|
|
224
|
+
|
|
225
|
+
if (!compact) {
|
|
226
|
+
throw new Error(`Unable to derive package slug from project name "${name}".`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return compact;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function toDisplayTitle(name) {
|
|
233
|
+
const cleaned = String(name || '').replace(/^@[^/]+\//, '');
|
|
234
|
+
const words = cleaned.split(/[-_\s]+/).filter(Boolean);
|
|
235
|
+
if (words.length === 0) return String(name || '').trim() || 'AuraJS Game';
|
|
236
|
+
return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveSkillsSourceDir() {
|
|
240
|
+
if (existsSync(ROOT_SKILLS_DIR)) return ROOT_SKILLS_DIR;
|
|
241
|
+
if (existsSync(PACKAGED_SKILLS_DIR)) return PACKAGED_SKILLS_DIR;
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function buildPackageJson({ name, version, license, binName, template }) {
|
|
246
|
+
const packageSlug = sanitizePackageSlug(name);
|
|
247
|
+
const packageName = `@aurajs/${packageSlug}`;
|
|
248
|
+
const keywords = ['aurajs', 'game'];
|
|
249
|
+
if (template.includes('3d')) {
|
|
250
|
+
keywords.push('3d');
|
|
251
|
+
keywords.push('platformer');
|
|
252
|
+
} else if (template.includes('2d')) {
|
|
253
|
+
keywords.push('2d');
|
|
254
|
+
keywords.push('shooter');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
name: packageName,
|
|
259
|
+
version,
|
|
260
|
+
description: `${toDisplayTitle(name)} — built with AuraJS`,
|
|
261
|
+
type: 'module',
|
|
262
|
+
bin: {
|
|
263
|
+
[binName]: './bin/play.js',
|
|
264
|
+
},
|
|
265
|
+
files: ['bin/', 'src/', 'assets/', 'skills/', 'aura.config.json'],
|
|
266
|
+
scripts: {
|
|
267
|
+
dev: 'aura dev',
|
|
268
|
+
build: 'aura build',
|
|
269
|
+
play: 'node ./bin/play.js',
|
|
270
|
+
publish: 'npx auramaxx publish',
|
|
271
|
+
},
|
|
272
|
+
dependencies: {
|
|
273
|
+
'@auraindustry/aurajs': '*',
|
|
274
|
+
},
|
|
275
|
+
keywords,
|
|
276
|
+
license,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function assertDestinationIsEmpty(name, dest) {
|
|
281
|
+
if (existsSync(dest)) {
|
|
282
|
+
const entries = readdirSync(dest);
|
|
283
|
+
if (entries.length > 0) {
|
|
284
|
+
throw new Error(`Directory "${name}" already exists and is not empty.`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
35
289
|
/**
|
|
36
290
|
* Recursively copy a directory tree, applying placeholder replacements
|
|
37
291
|
* to text files.
|