aether-engine 1.0.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.
Files changed (73) hide show
  1. package/README.md +15 -0
  2. package/biome.json +51 -0
  3. package/bun.lock +192 -0
  4. package/index.ts +1 -0
  5. package/package.json +25 -0
  6. package/serve.ts +125 -0
  7. package/src/audio/AudioEngine.ts +61 -0
  8. package/src/components/Animator3D.ts +65 -0
  9. package/src/components/AudioSource.ts +26 -0
  10. package/src/components/BitmapText.ts +25 -0
  11. package/src/components/Camera.ts +33 -0
  12. package/src/components/CameraFollow.ts +5 -0
  13. package/src/components/Collider.ts +16 -0
  14. package/src/components/Components.test.ts +68 -0
  15. package/src/components/Light.ts +15 -0
  16. package/src/components/MeshRenderer.ts +58 -0
  17. package/src/components/ParticleEmitter.ts +59 -0
  18. package/src/components/RigidBody.ts +9 -0
  19. package/src/components/ShadowCaster.ts +3 -0
  20. package/src/components/SkinnedMeshRenderer.ts +25 -0
  21. package/src/components/SpriteAnimator.ts +42 -0
  22. package/src/components/SpriteRenderer.ts +26 -0
  23. package/src/components/Transform.test.ts +39 -0
  24. package/src/components/Transform.ts +54 -0
  25. package/src/core/AssetManager.ts +123 -0
  26. package/src/core/Input.test.ts +67 -0
  27. package/src/core/Input.ts +94 -0
  28. package/src/core/Scene.ts +24 -0
  29. package/src/core/SceneManager.ts +57 -0
  30. package/src/core/Storage.ts +161 -0
  31. package/src/desktop/SteamClient.ts +52 -0
  32. package/src/ecs/System.ts +11 -0
  33. package/src/ecs/World.test.ts +29 -0
  34. package/src/ecs/World.ts +149 -0
  35. package/src/index.ts +115 -0
  36. package/src/math/Color.ts +100 -0
  37. package/src/math/Vector2.ts +96 -0
  38. package/src/math/Vector3.ts +103 -0
  39. package/src/math/math.test.ts +168 -0
  40. package/src/renderer/GlowMaterial.ts +66 -0
  41. package/src/renderer/LitMaterial.ts +337 -0
  42. package/src/renderer/Material.test.ts +23 -0
  43. package/src/renderer/Material.ts +80 -0
  44. package/src/renderer/OcclusionMaterial.ts +43 -0
  45. package/src/renderer/ParticleMaterial.ts +66 -0
  46. package/src/renderer/Shader.ts +44 -0
  47. package/src/renderer/SkinnedLitMaterial.ts +55 -0
  48. package/src/renderer/WaterMaterial.ts +298 -0
  49. package/src/renderer/WebGLRenderer.ts +917 -0
  50. package/src/systems/Animation3DSystem.ts +148 -0
  51. package/src/systems/AnimationSystem.ts +58 -0
  52. package/src/systems/AudioSystem.ts +62 -0
  53. package/src/systems/LightingSystem.ts +114 -0
  54. package/src/systems/ParticleSystem.ts +278 -0
  55. package/src/systems/PhysicsSystem.ts +211 -0
  56. package/src/systems/Systems.test.ts +165 -0
  57. package/src/systems/TextSystem.ts +153 -0
  58. package/src/ui/AnimationEditor.tsx +639 -0
  59. package/src/ui/BottomPanel.tsx +443 -0
  60. package/src/ui/EntityExplorer.tsx +420 -0
  61. package/src/ui/GameState.ts +286 -0
  62. package/src/ui/Icons.tsx +239 -0
  63. package/src/ui/InventoryPanel.tsx +335 -0
  64. package/src/ui/PlayerHUD.tsx +250 -0
  65. package/src/ui/SpriteEditor.tsx +3241 -0
  66. package/src/ui/SpriteSheetManager.tsx +198 -0
  67. package/src/utils/GLTFLoader.ts +257 -0
  68. package/src/utils/ObjLoader.ts +81 -0
  69. package/src/utils/idb.ts +137 -0
  70. package/src/utils/packer.ts +85 -0
  71. package/test_obj.ts +12 -0
  72. package/tsconfig.json +21 -0
  73. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,639 @@
1
+ import { createSignal, For, onCleanup, onMount, Show } from "solid-js";
2
+ import { type AetherEngine, SpriteAnimator, SpriteRenderer } from "../index";
3
+ import {
4
+ IconPause,
5
+ IconPlay,
6
+ IconPlus,
7
+ IconSave,
8
+ IconStepForward,
9
+ } from "./Icons";
10
+
11
+ export function AnimationEditor(props: {
12
+ engine: AetherEngine;
13
+ embedded?: boolean;
14
+ }) {
15
+ const [animators, setAnimators] = createSignal<
16
+ { id: number; comp: SpriteAnimator; rend?: SpriteRenderer }[]
17
+ >([]);
18
+ const [selectedId, setSelectedId] = createSignal<number>(-1);
19
+ const [version, setVersion] = createSignal(0);
20
+ const [fastTick, setFastTick] = createSignal(0);
21
+ const [saveStatus, setSaveStatus] = createSignal<string>("");
22
+
23
+ const [newName, setNewName] = createSignal("jump");
24
+ const [newFrames, setNewFrames] = createSignal("0,1");
25
+ const [newFps, setNewFps] = createSignal(12);
26
+
27
+ onMount(() => {
28
+ let frameId: number;
29
+ let slowInterval: any;
30
+
31
+ const tickLoop = () => {
32
+ setFastTick((t) => t + 1);
33
+ frameId = requestAnimationFrame(tickLoop);
34
+ };
35
+ tickLoop();
36
+
37
+ const pollEntities = () => {
38
+ const entities = props.engine.world.query(SpriteAnimator);
39
+ const list = entities.map((id) => ({
40
+ id,
41
+ comp: props.engine.world.getComponent(id, SpriteAnimator)!,
42
+ rend: props.engine.world.getComponent(id, SpriteRenderer) as
43
+ | SpriteRenderer
44
+ | undefined,
45
+ }));
46
+
47
+ setAnimators((prev) => {
48
+ if (prev.length !== list.length) return list;
49
+ for (let i = 0; i < prev.length; i++)
50
+ if (prev[i].id !== list[i].id) return list;
51
+ return prev;
52
+ });
53
+
54
+ if (selectedId() === -1 && list.length > 0) setSelectedId(list[0].id);
55
+ };
56
+ pollEntities();
57
+ slowInterval = setInterval(pollEntities, 1000);
58
+
59
+ onCleanup(() => {
60
+ cancelAnimationFrame(frameId);
61
+ clearInterval(slowInterval);
62
+ });
63
+ });
64
+
65
+ const getSelected = () => animators().find((a) => a.id === selectedId());
66
+ const parseFrames = (str: string) =>
67
+ str
68
+ .split(",")
69
+ .map((s) => parseInt(s.trim(), 10))
70
+ .filter((n) => !Number.isNaN(n));
71
+
72
+ return (
73
+ <div
74
+ style={
75
+ props.embedded
76
+ ? {
77
+ width: "100%",
78
+ height: "100%",
79
+ display: "flex",
80
+ gap: "5px",
81
+ "pointer-events": "auto",
82
+ color: "#EEEEEE",
83
+ }
84
+ : {
85
+ position: "fixed",
86
+ top: "50%",
87
+ left: "50%",
88
+ transform: "translate(-50%, -50%)",
89
+ width: "900px",
90
+ height: "600px",
91
+ background: "#383838",
92
+ padding: "5px",
93
+ "border-radius": "4px",
94
+ border: `1px solid #222222`,
95
+ "box-shadow": "0 10px 30px rgba(0,0,0,0.5)",
96
+ "font-family": "Arial, sans-serif",
97
+ "z-index": "9999",
98
+ display: "flex",
99
+ color: "#EEEEEE",
100
+ "pointer-events": "auto",
101
+ }
102
+ }
103
+ >
104
+ {/* Left Column: Selection & Playback */}
105
+ <div
106
+ style={{
107
+ width: "250px",
108
+ display: "flex",
109
+ "flex-direction": "column",
110
+ gap: "5px",
111
+ }}
112
+ >
113
+ <h3
114
+ style={{ margin: "5px 0 0 0", "font-size": "14px", color: "#EEEEEE" }}
115
+ >
116
+ Animation Timeline
117
+ </h3>
118
+ <p style={{ margin: 0, "font-size": "11px", color: "#B4B4B4" }}>
119
+ Manage sequence tags and active playback loops.
120
+ </p>
121
+
122
+ <label style={{ "font-size": "11px", color: "#B4B4B4" }}>
123
+ Target Entity:
124
+ </label>
125
+ <select
126
+ value={selectedId()}
127
+ onChange={(e) => setSelectedId(parseInt(e.currentTarget.value, 10))}
128
+ style={{
129
+ width: "100%",
130
+ padding: "4px",
131
+ "border-radius": "3px",
132
+ background: "#282828",
133
+ color: "#EEEEEE",
134
+ border: "1px solid #222222",
135
+ "font-size": "11px",
136
+ }}
137
+ >
138
+ <For each={animators()}>
139
+ {(a) => <option value={a.id}>Entity #{a.id}</option>}
140
+ </For>
141
+ </select>
142
+
143
+ <Show
144
+ when={getSelected()}
145
+ fallback={
146
+ <div style={{ "font-size": "11px", color: "#B4B4B4" }}>
147
+ No SpriteAnimators found in scene.
148
+ </div>
149
+ }
150
+ >
151
+ {(_selected) => {
152
+ const current = getSelected()?.comp;
153
+ return (
154
+ <div
155
+ style={{
156
+ background: "#2D2D2D",
157
+ padding: "5px",
158
+ "border-radius": "4px",
159
+ border: "1px solid #222222",
160
+ display: "flex",
161
+ "flex-direction": "column",
162
+ gap: "5px",
163
+ }}
164
+ >
165
+ <label style={{ "font-size": "11px", color: "#B4B4B4" }}>
166
+ Active Tag:
167
+ </label>
168
+ <select
169
+ value={version() > -1 ? current.currentAnimation : ""}
170
+ onChange={(e) => {
171
+ current.play(e.currentTarget.value);
172
+ setVersion((v) => v + 1);
173
+ }}
174
+ style={{
175
+ width: "100%",
176
+ padding: "4px",
177
+ "border-radius": "3px",
178
+ background: "#282828",
179
+ color: "#EEEEEE",
180
+ border: "1px solid #222222",
181
+ "font-size": "11px",
182
+ }}
183
+ >
184
+ <For
185
+ each={
186
+ version() > -1
187
+ ? Array.from(current.animations.keys())
188
+ : []
189
+ }
190
+ >
191
+ {(anim) => <option value={anim}>{anim}</option>}
192
+ </For>
193
+ </select>
194
+
195
+ <div style={{ display: "flex", gap: "6px" }}>
196
+ <button
197
+ onClick={() => {
198
+ current.isPlaying = !current.isPlaying;
199
+ setVersion((v) => v + 1);
200
+ }}
201
+ style={{
202
+ flex: 1,
203
+ padding: "5px",
204
+ "border-radius": "3px",
205
+ border: "1px solid #222222",
206
+ background:
207
+ version() > -1 && current.isPlaying
208
+ ? "#3A72B0"
209
+ : "#4A4A4A",
210
+ color: "#EEEEEE",
211
+ cursor: "pointer",
212
+ "font-size": "11px",
213
+ display: "flex",
214
+ "align-items": "center",
215
+ "justify-content": "center",
216
+ gap: "4px",
217
+ }}
218
+ >
219
+ {version() > -1 && current.isPlaying ? (
220
+ <>
221
+ <IconPause size={12} /> Pause
222
+ </>
223
+ ) : (
224
+ <>
225
+ <IconPlay size={12} /> Play
226
+ </>
227
+ )}
228
+ </button>
229
+ <button
230
+ onClick={() => {
231
+ current.isPlaying = false;
232
+ const anim = current.animations.get(
233
+ current.currentAnimation,
234
+ );
235
+ if (anim)
236
+ current.currentFrameIndex =
237
+ (current.currentFrameIndex + 1) % anim.frames.length;
238
+ setVersion((v) => v + 1);
239
+ }}
240
+ style={{
241
+ flex: 1,
242
+ padding: "5px",
243
+ "border-radius": "3px",
244
+ border: "1px solid #222222",
245
+ background: "#4A4A4A",
246
+ color: "#EEEEEE",
247
+ cursor: "pointer",
248
+ "font-size": "11px",
249
+ display: "flex",
250
+ "align-items": "center",
251
+ "justify-content": "center",
252
+ gap: "4px",
253
+ }}
254
+ >
255
+ Step <IconStepForward size={12} />
256
+ </button>
257
+ </div>
258
+
259
+ <div
260
+ style={{
261
+ "text-align": "right",
262
+ "font-size": "10px",
263
+ color: "#B4B4B4",
264
+ }}
265
+ >
266
+ Frame Index:{" "}
267
+ <span style={{ color: "#EEEEEE", "font-weight": "bold" }}>
268
+ {(() => {
269
+ fastTick();
270
+ return current.currentFrameIndex;
271
+ })()}
272
+ </span>
273
+ </div>
274
+ </div>
275
+ );
276
+ }}
277
+ </Show>
278
+ </div>
279
+
280
+ {/* Right Column: Timeline Tags / Sequences */}
281
+ <Show when={getSelected()}>
282
+ {(_selected) => {
283
+ const state = getSelected()!;
284
+ const current = state.comp;
285
+ return (
286
+ <div
287
+ style={{
288
+ flex: 1,
289
+ display: "flex",
290
+ "flex-direction": "column",
291
+ gap: "5px",
292
+ background: "#2D2D2D",
293
+ padding: "5px",
294
+ "border-radius": "4px",
295
+ border: "1px solid #222222",
296
+ }}
297
+ >
298
+ <div
299
+ style={{
300
+ display: "flex",
301
+ "justify-content": "space-between",
302
+ "align-items": "center",
303
+ }}
304
+ >
305
+ <h4
306
+ style={{ margin: 0, "font-size": "12px", color: "#EEEEEE" }}
307
+ >
308
+ Timeline Tracker
309
+ </h4>
310
+
311
+ <Show when={state.rend?.imageSrc?.includes(".png")}>
312
+ <div
313
+ style={{
314
+ display: "flex",
315
+ "align-items": "center",
316
+ gap: "10px",
317
+ }}
318
+ >
319
+ <span
320
+ style={{
321
+ "font-size": "10px",
322
+ color:
323
+ saveStatus() === "Saved"
324
+ ? "#4CAF50"
325
+ : saveStatus() === "Error"
326
+ ? "#F44336"
327
+ : "#888",
328
+ }}
329
+ >
330
+ {saveStatus()}
331
+ </span>
332
+ <button
333
+ onClick={async () => {
334
+ setSaveStatus("Saving...");
335
+ const name = state.rend?.imageSrc
336
+ .replace("/public/assets/sprites/", "")
337
+ .replace(".png", "");
338
+ if (!name) return setSaveStatus("Error");
339
+
340
+ try {
341
+ const res = await fetch(
342
+ `/public/assets/sprites/${name}.json`,
343
+ );
344
+ const meta = res.ok ? await res.json() : {};
345
+ meta.animations = meta.animations || {};
346
+
347
+ // Overwrite animations exactly as configured
348
+ current.animations.forEach((def, key) => {
349
+ meta.animations[key] = {
350
+ frames: Array.from(def.frames),
351
+ fps: def.fps,
352
+ };
353
+ });
354
+
355
+ // Check for deleted animations
356
+ Object.keys(meta.animations).forEach((k) => {
357
+ if (!current.animations.has(k))
358
+ delete meta.animations[k];
359
+ });
360
+
361
+ const saveResp = await fetch(
362
+ "/__aether_dev/save-sprite-meta",
363
+ {
364
+ method: "POST",
365
+ headers: { "Content-Type": "application/json" },
366
+ body: JSON.stringify({ name, metadata: meta }),
367
+ },
368
+ );
369
+ if (!saveResp.ok) throw new Error();
370
+
371
+ setSaveStatus("Saved");
372
+ setTimeout(() => setSaveStatus("Ready"), 2000);
373
+ } catch (_e) {
374
+ setSaveStatus("Error");
375
+ }
376
+ }}
377
+ style={{
378
+ padding: "4px 8px",
379
+ background: "#3A72B0",
380
+ color: "#EEEEEE",
381
+ border: "1px solid #222222",
382
+ cursor: "pointer",
383
+ "font-size": "11px",
384
+ "font-weight": "bold",
385
+ "border-radius": "3px",
386
+ display: "flex",
387
+ "align-items": "center",
388
+ gap: "4px",
389
+ }}
390
+ >
391
+ <IconSave size={12} /> Save to Disk
392
+ </button>
393
+ </div>
394
+ </Show>
395
+ </div>
396
+
397
+ <div
398
+ style={{
399
+ display: "flex",
400
+ gap: "5px",
401
+ "font-size": "11px",
402
+ color: "#B4B4B4",
403
+ "border-bottom": "1px solid #222222",
404
+ "padding-bottom": "5px",
405
+ }}
406
+ >
407
+ <div style={{ width: "100px" }}>Tag Name</div>
408
+ <div style={{ flex: 1 }}>Sequence</div>
409
+ <div style={{ width: "60px" }}>FPS</div>
410
+ <div style={{ width: "60px" }}>Actions</div>
411
+ </div>
412
+
413
+ <div
414
+ style={{
415
+ flex: 1,
416
+ "overflow-y": "auto",
417
+ display: "flex",
418
+ "flex-direction": "column",
419
+ gap: "5px",
420
+ }}
421
+ >
422
+ <For
423
+ each={
424
+ version() > -1
425
+ ? Array.from(current.animations.entries())
426
+ : []
427
+ }
428
+ >
429
+ {([name, data]) => (
430
+ <div
431
+ style={{
432
+ display: "flex",
433
+ gap: "5px",
434
+ "align-items": "center",
435
+ "font-size": "11px",
436
+ }}
437
+ >
438
+ <input
439
+ type="text"
440
+ value={name}
441
+ disabled
442
+ style={{
443
+ width: "100px",
444
+ padding: "4px",
445
+ background: "#383838",
446
+ border: "1px solid #222222",
447
+ color: "#EEEEEE",
448
+ }}
449
+ />
450
+
451
+ <input
452
+ type="text"
453
+ value={Array.from(data.frames).join(", ")}
454
+ onInput={(e) => {
455
+ data.frames = parseFrames(
456
+ e.currentTarget.value,
457
+ ) as any;
458
+ if (current.currentAnimation === name)
459
+ current.play(name);
460
+ }}
461
+ style={{
462
+ flex: 1,
463
+ padding: "4px",
464
+ background: "#282828",
465
+ border: "1px solid #222222",
466
+ color: "#EEEEEE",
467
+ }}
468
+ />
469
+
470
+ <input
471
+ type="number"
472
+ min="1"
473
+ max="60"
474
+ value={data.fps}
475
+ onInput={(e) =>
476
+ (data.fps = parseInt(e.currentTarget.value, 10) || 12)
477
+ }
478
+ style={{
479
+ width: "60px",
480
+ padding: "4px",
481
+ background: "#282828",
482
+ border: "1px solid #222222",
483
+ color: "#EEEEEE",
484
+ }}
485
+ />
486
+
487
+ <button
488
+ onClick={() => {
489
+ current.animations.delete(name);
490
+ if (current.currentAnimation === name)
491
+ current.play(
492
+ Array.from(current.animations.keys())[0] || "",
493
+ );
494
+ setVersion((v) => v + 1);
495
+ }}
496
+ style={{
497
+ width: "60px",
498
+ padding: "4px",
499
+ background: "#4A4A4A",
500
+ color: "#EEEEEE",
501
+ border: "1px solid #222222",
502
+ cursor: "pointer",
503
+ "border-radius": "3px",
504
+ }}
505
+ >
506
+ Delete
507
+ </button>
508
+ </div>
509
+ )}
510
+ </For>
511
+ </div>
512
+
513
+ <div
514
+ style={{
515
+ "border-top": "1px solid #222222",
516
+ "padding-top": "10px",
517
+ display: "flex",
518
+ gap: "5px",
519
+ "align-items": "flex-end",
520
+ }}
521
+ >
522
+ <div>
523
+ <label
524
+ style={{
525
+ "font-size": "10px",
526
+ color: "#B4B4B4",
527
+ display: "block",
528
+ "margin-bottom": "3px",
529
+ }}
530
+ >
531
+ New Sequence
532
+ </label>
533
+ <input
534
+ type="text"
535
+ value={newName()}
536
+ onInput={(e) => setNewName(e.currentTarget.value)}
537
+ style={{
538
+ width: "100px",
539
+ padding: "4px",
540
+ background: "#282828",
541
+ border: "1px solid #222222",
542
+ color: "#EEEEEE",
543
+ "font-size": "11px",
544
+ "border-radius": "3px",
545
+ }}
546
+ />
547
+ </div>
548
+ <div style={{ flex: 1 }}>
549
+ <label
550
+ style={{
551
+ "font-size": "10px",
552
+ color: "#B4B4B4",
553
+ display: "block",
554
+ "margin-bottom": "3px",
555
+ }}
556
+ >
557
+ Frames (CSV)
558
+ </label>
559
+ <input
560
+ type="text"
561
+ value={newFrames()}
562
+ onInput={(e) => setNewFrames(e.currentTarget.value)}
563
+ placeholder="0,1,2"
564
+ style={{
565
+ width: "100%",
566
+ padding: "4px",
567
+ background: "#282828",
568
+ border: "1px solid #222222",
569
+ color: "#EEEEEE",
570
+ "font-size": "11px",
571
+ "border-radius": "3px",
572
+ }}
573
+ />
574
+ </div>
575
+ <div>
576
+ <label
577
+ style={{
578
+ "font-size": "10px",
579
+ color: "#B4B4B4",
580
+ display: "block",
581
+ "margin-bottom": "3px",
582
+ }}
583
+ >
584
+ FPS
585
+ </label>
586
+ <input
587
+ type="number"
588
+ value={newFps()}
589
+ onInput={(e) =>
590
+ setNewFps(parseInt(e.currentTarget.value, 10))
591
+ }
592
+ style={{
593
+ width: "60px",
594
+ padding: "4px",
595
+ background: "#282828",
596
+ border: "1px solid #222222",
597
+ color: "#EEEEEE",
598
+ "font-size": "11px",
599
+ "border-radius": "3px",
600
+ }}
601
+ />
602
+ </div>
603
+ <button
604
+ onClick={() => {
605
+ if (newName() && newFrames()) {
606
+ const arr = parseFrames(newFrames());
607
+ if (arr.length > 0) {
608
+ current.addAnimation(newName(), arr, newFps());
609
+ setNewName("");
610
+ setVersion((v) => v + 1);
611
+ }
612
+ }
613
+ }}
614
+ style={{
615
+ padding: "4px 10px",
616
+ height: "25px",
617
+ background: "#3A72B0",
618
+ color: "#EEEEEE",
619
+ border: "1px solid #222222",
620
+ cursor: "pointer",
621
+ "font-size": "11px",
622
+ "font-weight": "bold",
623
+ "border-radius": "3px",
624
+ display: "flex",
625
+ "align-items": "center",
626
+ "justify-content": "center",
627
+ gap: "4px",
628
+ }}
629
+ >
630
+ <IconPlus size={12} /> Add
631
+ </button>
632
+ </div>
633
+ </div>
634
+ );
635
+ }}
636
+ </Show>
637
+ </div>
638
+ );
639
+ }