@aethervtt/shard-project 0.0.1

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.
@@ -0,0 +1,283 @@
1
+ const json = (value) => `${JSON.stringify(value, null, 2)}
2
+ `;
3
+ function manifest(name) {
4
+ return {
5
+ $schema: "./.shard/schemas/shard.schema.json",
6
+ name,
7
+ version: "0.1.0",
8
+ engine: "0.x",
9
+ entry: "scripts/main.ts",
10
+ startScene: "scenes/main.scene.json",
11
+ seed: 1,
12
+ window: { width: 1280, height: 720, msaa: 4 },
13
+ plugins: ["render/forward", "input"]
14
+ };
15
+ }
16
+ const emptyScene = {
17
+ $schema: "../.shard/schemas/scene.schema.json",
18
+ version: 1,
19
+ assets: { red: { type: "Material", value: { baseColor: "#c0392b", roughness: 0.4 } } },
20
+ resources: { "render/AmbientLight": { color: [1, 1, 1], brightness: 500 } },
21
+ entities: [
22
+ {
23
+ name: "sun",
24
+ components: {
25
+ "render/DirectionalLight": { illuminance: "daylight" },
26
+ "core/Transform": { rotationEuler: [-50, 30, 0] }
27
+ }
28
+ },
29
+ {
30
+ name: "camera",
31
+ components: {
32
+ "render/Camera3d": { fovY: 60 },
33
+ "core/Transform": { translation: [0, 2, 6], rotationEuler: [-15, 0, 0] }
34
+ }
35
+ },
36
+ {
37
+ name: "cube",
38
+ components: {
39
+ "render/Mesh3d": { mesh: { path: "procedural:cube?size=1.5" } },
40
+ "render/MeshMaterial": { material: { path: "#red" } },
41
+ "core/Transform": { translation: [0, 0.5, 0] }
42
+ }
43
+ }
44
+ ]
45
+ };
46
+ const explorerScene = (name) => ({
47
+ $schema: "../.shard/schemas/scene.schema.json",
48
+ version: 1,
49
+ assets: {
50
+ rock: { type: "Material", value: { baseColor: "#7a5c48", roughness: 0.95 } },
51
+ ice: { type: "Material", value: { baseColor: "#c9d6df", roughness: 0.6 } },
52
+ hull: { type: "Material", value: { baseColor: "#d35400", roughness: 0.35 } },
53
+ glass: {
54
+ type: "Material",
55
+ value: { baseColor: "#1b2631", roughness: 0.1, emissive: "#5dade2", emissiveLuminance: 400 }
56
+ }
57
+ },
58
+ resources: { "render/AmbientLight": { color: [0.6, 0.7, 1], brightness: 300 } },
59
+ entities: [
60
+ {
61
+ name: "sun",
62
+ components: {
63
+ "render/DirectionalLight": { illuminance: "daylight" },
64
+ "core/Transform": { rotationEuler: [-25, 50, 0] }
65
+ }
66
+ },
67
+ {
68
+ name: "planet",
69
+ components: {
70
+ "render/Mesh3d": { mesh: { path: "procedural:sphere?radius=40&segments=64" } },
71
+ "render/MeshMaterial": { material: { path: "#rock" } },
72
+ "core/Transform": { translation: [0, -20, -140] }
73
+ },
74
+ children: [
75
+ {
76
+ name: "moon",
77
+ components: {
78
+ "render/Mesh3d": { mesh: { path: "procedural:sphere?radius=6&segments=32" } },
79
+ "render/MeshMaterial": { material: { path: "#ice" } },
80
+ "core/Transform": { translation: [70, 35, 20] }
81
+ }
82
+ }
83
+ ]
84
+ },
85
+ {
86
+ name: "ship",
87
+ components: {
88
+ [`${name}/Ship`]: {},
89
+ "render/Mesh3d": { mesh: { path: "procedural:box?x=1.6&y=0.5&z=3.2" } },
90
+ "render/MeshMaterial": { material: { path: "#hull" } },
91
+ "core/Transform": {}
92
+ },
93
+ children: [
94
+ {
95
+ name: "cockpit",
96
+ components: {
97
+ "render/Mesh3d": { mesh: { path: "procedural:sphere?radius=0.45" } },
98
+ "render/MeshMaterial": { material: { path: "#glass" } },
99
+ "core/Transform": { translation: [0, 0.3, -0.6] }
100
+ }
101
+ },
102
+ {
103
+ name: "camera",
104
+ components: {
105
+ "render/Camera3d": { fovY: 65 },
106
+ "core/Transform": { translation: [0, 2.2, 8], rotationEuler: [-10, 0, 0] }
107
+ }
108
+ }
109
+ ]
110
+ }
111
+ ]
112
+ });
113
+ const explorerMain = (name) => `import { defineSystem, FixedUpdate, quat, t, vec3 } from '@aethervtt/shard-core'
114
+ import { addActions, defineActions } from '@aethervtt/shard-input'
115
+ import { defineProject } from '@aethervtt/shard-project'
116
+ import { FixedTime } from '@aethervtt/shard-runtime'
117
+ import { Transform } from '@aethervtt/shard-transform'
118
+
119
+ /** Flight controls. Bindings are data: an agent (or player) can remap them without code changes. */
120
+ export const Controls = defineActions('${name}/Controls', {
121
+ thrust: { kind: 'button', bindings: ['Key:KeyW', 'Key:Space', 'Gamepad:RightTrigger'] },
122
+ steer: { kind: 'axis2d', bindings: [{ composite: 'arrows' }, 'Gamepad:LeftStick'], deadZone: 0.15 },
123
+ })
124
+
125
+ const project = defineProject({
126
+ name: '${name}',
127
+ build(app) {
128
+ addActions(app.world, Controls)
129
+ app.addSystems(FixedUpdate, fly)
130
+ },
131
+ })
132
+
133
+ export const Ship = project.component(
134
+ 'Ship',
135
+ {
136
+ speed: t.f32({ min: 0, unit: 'm/s', description: 'Current forward speed.' }),
137
+ maxSpeed: t.f32({ default: 40, min: 0, unit: 'm/s', description: 'Speed cap.' }),
138
+ acceleration: t.f32({ default: 15, min: 0, unit: 'm/s\xB2', description: 'Thrust acceleration.' }),
139
+ drag: t.f32({ default: 0.4, min: 0, description: 'Fraction of speed lost per second without thrust.' }),
140
+ turnRate: t.f32({ default: 1.2, min: 0, unit: 'rad/s', description: 'Steering speed.' }),
141
+ },
142
+ { description: 'A ship the player flies: steer with arrows or the left stick, thrust with W.' },
143
+ )
144
+
145
+ const spin = quat.create()
146
+ const forward = vec3.create()
147
+
148
+ /** Steers and moves every ship along its -Z axis. */
149
+ const fly = defineSystem({
150
+ name: '${name}/fly',
151
+ setup: (world) => ({ ships: world.query({ with: [Ship, Transform] }) }),
152
+ run: ({ ships }, world) => {
153
+ const dt = world.resource(FixedTime).step
154
+ const controls = world.resource(Controls.resource)
155
+ const [yaw, pitch] = controls.axis2d('steer')
156
+ const thrust = controls.pressed('thrust')
157
+ for (const table of ships.tables) {
158
+ const speed = table.column(Ship, 'speed')
159
+ const maxSpeed = table.column(Ship, 'maxSpeed')
160
+ const acceleration = table.column(Ship, 'acceleration')
161
+ const drag = table.column(Ship, 'drag')
162
+ const turnRate = table.column(Ship, 'turnRate')
163
+ const translation = table.column(Transform, 'translation')
164
+ const rotation = table.column(Transform, 'rotation')
165
+ for (let i = 0; i < table.count; i++) {
166
+ const s = thrust
167
+ ? Math.min(maxSpeed[i]!, speed[i]! + acceleration[i]! * dt)
168
+ : speed[i]! * Math.max(0, 1 - drag[i]! * dt)
169
+ speed[i] = s
170
+ const r = rotation.subarray(i * 4, i * 4 + 4)
171
+ quat.fromEuler(spin, pitch * turnRate[i]! * dt, -yaw * turnRate[i]! * dt, 0)
172
+ quat.normalize(r, quat.multiply(r, r, spin))
173
+ vec3.transformQuat(forward, [0, 0, -1], r)
174
+ translation[i * 3] = translation[i * 3]! + forward[0]! * s * dt
175
+ translation[i * 3 + 1] = translation[i * 3 + 1]! + forward[1]! * s * dt
176
+ translation[i * 3 + 2] = translation[i * 3 + 2]! + forward[2]! * s * dt
177
+ }
178
+ table.markChanged(Ship)
179
+ table.markChanged(Transform)
180
+ }
181
+ },
182
+ })
183
+
184
+ export default project
185
+ `;
186
+ const emptyMain = (name) => `import { defineProject } from '@aethervtt/shard-project'
187
+
188
+ const project = defineProject({
189
+ name: '${name}',
190
+ build(_app) {
191
+ // Register systems here: app.addSystems(Update, mySystem)
192
+ },
193
+ })
194
+
195
+ export default project
196
+ `;
197
+ const explorerTest = (name) => `import { expect, test } from '@aethervtt/shard-testing'
198
+
199
+ test('holding thrust flies the ship forward', async ({ game }) => {
200
+ await game.load('scenes/main.scene.json')
201
+ game.input.hold('${name}/Controls.thrust')
202
+ await game.step(120)
203
+ const ship = game.get('ship', '${name}/Ship')
204
+ expect(ship.speed).toBeGreaterThan(10)
205
+ expect(game.get('ship', 'core/Transform').translation[2]).toBeLessThan(-10)
206
+ await game.screenshot('thrust.png')
207
+ })
208
+
209
+ test('the ship coasts to a stop without thrust', async ({ game }) => {
210
+ await game.load('scenes/main.scene.json')
211
+ await game.step(60)
212
+ expect(game.get('ship', '${name}/Ship').speed).toBe(0)
213
+ })
214
+ `;
215
+ const emptyTest = `import { expect, test } from '@aethervtt/shard-testing'
216
+
217
+ test('the start scene loads', async ({ game }) => {
218
+ await game.load('scenes/main.scene.json')
219
+ await game.step(10)
220
+ expect(game.get('cube', 'core/Transform').translation[1]).toBe(0.5)
221
+ })
222
+ `;
223
+ function projectTemplate(options) {
224
+ const { name, template, engineSpec } = options;
225
+ const deps = Object.fromEntries(
226
+ [
227
+ "@aethervtt/shard-cli",
228
+ "@aethervtt/shard-core",
229
+ "@aethervtt/shard-input",
230
+ "@aethervtt/shard-project",
231
+ "@aethervtt/shard-runtime",
232
+ "@aethervtt/shard-testing",
233
+ "@aethervtt/shard-transform"
234
+ ].map((p) => [p, engineSpec])
235
+ );
236
+ return {
237
+ "shard.json": json(manifest(name)),
238
+ "package.json": json({
239
+ name,
240
+ private: true,
241
+ type: "module",
242
+ scripts: {
243
+ validate: "shard validate",
244
+ test: "shard test",
245
+ check: "shard check",
246
+ dev: "shard dev",
247
+ docs: "shard docs"
248
+ },
249
+ dependencies: deps,
250
+ // Engine packages ship as TypeScript source, so type checks need their ambient types.
251
+ devDependencies: {
252
+ "@types/node": "^26.6.2",
253
+ "@webgpu/types": "^0.1.74",
254
+ typescript: "^7.0.2"
255
+ }
256
+ }),
257
+ "scripts/main.ts": template === "explorer" ? explorerMain(name) : emptyMain(name),
258
+ "scenes/main.scene.json": json(template === "explorer" ? explorerScene(name) : emptyScene),
259
+ "tests/main.test.ts": template === "explorer" ? explorerTest(name) : emptyTest,
260
+ "shaders/.gitkeep": "",
261
+ "assets/.gitkeep": "",
262
+ ".gitignore": ".shard/\nnode_modules/\n",
263
+ ".mcp.json": json({
264
+ mcpServers: { shard: { command: "pnpm", args: ["exec", "shard", "mcp"] } }
265
+ }),
266
+ "tsconfig.json": json({
267
+ compilerOptions: {
268
+ target: "ES2023",
269
+ lib: ["ES2023", "DOM"],
270
+ module: "ESNext",
271
+ moduleResolution: "bundler",
272
+ strict: true,
273
+ noEmit: true,
274
+ skipLibCheck: true,
275
+ types: ["node", "@webgpu/types"]
276
+ },
277
+ include: ["scripts", "tests"]
278
+ })
279
+ };
280
+ }
281
+ export {
282
+ projectTemplate
283
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@aethervtt/shard-project",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "LICENSE",
16
+ "README.md"
17
+ ],
18
+ "dependencies": {
19
+ "@aethervtt/shard-animation": "0.0.1",
20
+ "@aethervtt/shard-assets": "0.0.1",
21
+ "@aethervtt/shard-audio": "0.0.1",
22
+ "@aethervtt/shard-core": "0.0.1",
23
+ "@aethervtt/shard-gltf": "0.0.1",
24
+ "@aethervtt/shard-gpu": "0.0.1",
25
+ "@aethervtt/shard-input": "0.0.1",
26
+ "@aethervtt/shard-nav": "0.0.1",
27
+ "@aethervtt/shard-noise": "0.0.1",
28
+ "@aethervtt/shard-particles": "0.0.1",
29
+ "@aethervtt/shard-physics": "0.0.1",
30
+ "@aethervtt/shard-platform": "0.0.1",
31
+ "@aethervtt/shard-procgen": "0.0.1",
32
+ "@aethervtt/shard-protocol": "0.0.1",
33
+ "@aethervtt/shard-render": "0.0.1",
34
+ "@aethervtt/shard-runtime": "0.0.1",
35
+ "@aethervtt/shard-save": "0.0.1",
36
+ "@aethervtt/shard-scene": "0.0.1",
37
+ "@aethervtt/shard-sprite": "0.0.1",
38
+ "@aethervtt/shard-terrain": "0.0.1",
39
+ "@aethervtt/shard-text": "0.0.1",
40
+ "@aethervtt/shard-transform": "0.0.1",
41
+ "@aethervtt/shard-ui": "0.0.1"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/aethervtt/shard.git",
46
+ "directory": "packages/project"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public",
50
+ "provenance": true
51
+ },
52
+ "shard": {
53
+ "buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
54
+ }
55
+ }