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,16 @@
1
+ import { vec3 } from "gl-matrix";
2
+
3
+ export class Collider {
4
+ public type: "AABB" | "Sphere" = "AABB";
5
+
6
+ public size: vec3 = vec3.fromValues(1, 1, 1); // For AABB
7
+ public radius: number = 0.5; // For Sphere
8
+ public centerOffset: vec3 = vec3.create();
9
+
10
+ public isTrigger: boolean = false;
11
+
12
+ // Collision state tracking
13
+ public collisions: number[] = []; // Entity IDs we currently overlap
14
+ public enterCollisions: number[] = []; // Entity IDs we just started overlapping this frame
15
+ public exitCollisions: number[] = []; // Entity IDs we just stopped overlapping this frame
16
+ }
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BitmapText } from "./BitmapText";
3
+ import { Collider } from "./Collider";
4
+ import { Light } from "./Light";
5
+ import { MeshRenderer } from "./MeshRenderer";
6
+ import { ParticleEmitter } from "./ParticleEmitter";
7
+ import { RigidBody } from "./RigidBody";
8
+
9
+ describe("ECS Components", () => {
10
+ test("RigidBody defaults", () => {
11
+ const rb = new RigidBody();
12
+ expect(rb.velocity[0]).toBe(0);
13
+ expect(rb.isKinematic).toBe(false);
14
+ expect(rb.mass).toBe(1.0);
15
+ });
16
+
17
+ test("Collider intersection mapping", () => {
18
+ const c1 = new Collider();
19
+ c1.size[0] = 10;
20
+ c1.size[1] = 10;
21
+ expect(c1.size[0]).toBe(10);
22
+ expect(c1.size[1]).toBe(10);
23
+ expect(c1.isTrigger).toBe(false);
24
+ });
25
+
26
+ test("Light types and parameters", () => {
27
+ const l = new Light("Ambient");
28
+ expect(l.type).toBe("Ambient");
29
+ expect(l.intensity).toBe(1.0);
30
+
31
+ const l2 = new Light("Point");
32
+ expect(l2.type).toBe("Point");
33
+ expect(l2.radius).toBe(100.0);
34
+ });
35
+
36
+ test("ParticleEmitter initializes buffers correctly", () => {
37
+ const pe = new ParticleEmitter(500);
38
+ expect(pe.maxParticles).toBe(500);
39
+ expect(pe.pLife.length).toBe(500);
40
+ expect(pe.pPosX.length).toBe(500);
41
+ });
42
+
43
+ test("BitmapText sets dirty flag on text change", () => {
44
+ const bt = new BitmapText("Hello", "font1");
45
+ bt.isDirty = false;
46
+ bt.text = "World";
47
+ expect(bt.isDirty).toBe(true);
48
+ expect(bt.text).toBe("World");
49
+
50
+ // Setting same string should not dirty
51
+ bt.isDirty = false;
52
+ bt.text = "World";
53
+ expect(bt.isDirty).toBe(false);
54
+ });
55
+
56
+ test("MeshRenderer processes advanced geometry arrays", () => {
57
+ const positions = [0, 0, 0, 1, 0, 0, 1, 1, 0];
58
+ const uvs = [0, 0, 1, 0, 1, 1];
59
+ const indices = [0, 1, 2];
60
+ const colors = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
61
+
62
+ const mesh = new MeshRenderer(positions, undefined, colors, uvs, indices);
63
+ expect(mesh.vertexCount).toBe(3);
64
+ expect(mesh.indexCount).toBe(3);
65
+ expect(mesh.uvs?.length).toBe(6);
66
+ expect(mesh.indices?.length).toBe(3);
67
+ });
68
+ });
@@ -0,0 +1,15 @@
1
+ import { Color } from "../math/Color";
2
+
3
+ export class Light {
4
+ public type: "Point" | "Ambient" | "Directional" = "Point";
5
+ public color: Color = Color.White();
6
+ public intensity: number = 1.0;
7
+ public radius: number = 100.0; // Falloff for point lights
8
+ public elevation: number = 0.0; // Z-axis physical offset from its transform base
9
+ public positionOffset: [number, number, number] = [0, 0, 0]; // Visual screen-space offset
10
+ public flickerIntensity: number = 0.0; // Mixes in procedural flicker
11
+
12
+ constructor(type: "Point" | "Ambient" | "Directional" = "Point") {
13
+ this.type = type;
14
+ }
15
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ DEFAULT_MESH_FS,
3
+ DEFAULT_MESH_VS,
4
+ Material,
5
+ } from "../renderer/Material";
6
+
7
+ export class MeshRenderer {
8
+ public positions: Float32Array;
9
+ public normals?: Float32Array;
10
+ public colors?: Float32Array;
11
+ public uvs?: Float32Array;
12
+ public indices?: Uint16Array;
13
+
14
+ public positionBuffer?: WebGLBuffer;
15
+ public normalBuffer?: WebGLBuffer;
16
+ public colorBuffer?: WebGLBuffer;
17
+ public uvBuffer?: WebGLBuffer;
18
+ public indexBuffer?: WebGLBuffer;
19
+
20
+ public vertexCount: number = 0;
21
+ public indexCount: number = 0;
22
+ public isDirty: boolean = false;
23
+
24
+ public material: Material;
25
+
26
+ constructor(
27
+ positions: number[],
28
+ normals?: number[],
29
+ colors?: number[],
30
+ uvs?: number[],
31
+ indices?: number[],
32
+ material?: Material,
33
+ ) {
34
+ this.material = material || new Material(DEFAULT_MESH_VS, DEFAULT_MESH_FS);
35
+ this.positions = new Float32Array(positions);
36
+ this.vertexCount = Math.floor(positions.length / 3);
37
+
38
+ if (normals) {
39
+ this.normals = new Float32Array(normals);
40
+ }
41
+
42
+ if (colors) {
43
+ if (colors.length / 4 !== this.vertexCount) {
44
+ console.warn("Color array length does not match vertex count expected");
45
+ }
46
+ this.colors = new Float32Array(colors);
47
+ }
48
+
49
+ if (uvs) {
50
+ this.uvs = new Float32Array(uvs);
51
+ }
52
+
53
+ if (indices) {
54
+ this.indices = new Uint16Array(indices);
55
+ this.indexCount = indices.length;
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,59 @@
1
+ import { Color } from "../math/Color";
2
+ import { Material } from "../renderer/Material";
3
+
4
+ export class ParticleEmitter {
5
+ public maxParticles: number;
6
+ public emissionRate: number = 100;
7
+ public emitAccumulator: number = 0;
8
+
9
+ public minSpeed: number = 2.0;
10
+ public maxSpeed: number = 5.0;
11
+ public lifespan: number = 1.0;
12
+
13
+ public startColor: Color = Color.White();
14
+ public endColor: Color = Color.White();
15
+ public startSize: number = 0.5;
16
+ public endSize: number = 0.1;
17
+
18
+ // Tailored Tweakable Options
19
+ public material?: Material;
20
+ public billboard: boolean = true;
21
+ public shape: "square" | "circle" = "square";
22
+ public depthWrite: boolean = false;
23
+ public blendMode: "Alpha" | "Additive" = "Additive";
24
+
25
+ public lightRadius: number = 2.236; // Default to old hardcoded ~2.236 radius (radiusSq = 5)
26
+
27
+ public activeCount: number = 0;
28
+
29
+ public useSuperformula: boolean = false;
30
+ public superformulaMode: "velocity" | "position" = "velocity";
31
+ public superformula = {
32
+ m: 5,
33
+ n1: 1,
34
+ n2: 1,
35
+ n3: 1,
36
+ a: 1,
37
+ b: 1,
38
+ };
39
+
40
+ public pLife: Float32Array;
41
+ public pPosX: Float32Array;
42
+ public pPosY: Float32Array;
43
+ public pPosZ: Float32Array;
44
+ public pVelX: Float32Array;
45
+ public pVelY: Float32Array;
46
+ public pVelZ: Float32Array;
47
+
48
+ constructor(maxParticles: number = 1000) {
49
+ this.maxParticles = maxParticles;
50
+
51
+ this.pLife = new Float32Array(maxParticles);
52
+ this.pPosX = new Float32Array(maxParticles);
53
+ this.pPosY = new Float32Array(maxParticles);
54
+ this.pPosZ = new Float32Array(maxParticles);
55
+ this.pVelX = new Float32Array(maxParticles);
56
+ this.pVelY = new Float32Array(maxParticles);
57
+ this.pVelZ = new Float32Array(maxParticles);
58
+ }
59
+ }
@@ -0,0 +1,9 @@
1
+ import { vec3 } from "gl-matrix";
2
+
3
+ export class RigidBody {
4
+ public velocity: vec3 = vec3.create();
5
+ public acceleration: vec3 = vec3.create();
6
+ public mass: number = 1.0;
7
+ public gravityScale: number = 1.0;
8
+ public isKinematic: boolean = false;
9
+ }
@@ -0,0 +1,3 @@
1
+ export class ShadowCaster {
2
+ // A pure marker component signifying this entity blocks light rays
3
+ }
@@ -0,0 +1,25 @@
1
+ import { Material } from "../renderer/Material";
2
+ import { MeshRenderer } from "./MeshRenderer";
3
+
4
+ export class SkinnedMeshRenderer extends MeshRenderer {
5
+ public joints: Float32Array;
6
+ public weights: Float32Array;
7
+
8
+ public jointBuffer?: WebGLBuffer;
9
+ public weightBuffer?: WebGLBuffer;
10
+
11
+ constructor(
12
+ positions: number[],
13
+ normals: number[],
14
+ colors: number[] | undefined,
15
+ uvs: number[],
16
+ indices: number[] | undefined,
17
+ joints: number[],
18
+ weights: number[],
19
+ material?: Material,
20
+ ) {
21
+ super(positions, normals, colors, uvs, indices, material);
22
+ this.joints = new Float32Array(joints);
23
+ this.weights = new Float32Array(weights);
24
+ }
25
+ }
@@ -0,0 +1,42 @@
1
+ export type AnimationDefinition = {
2
+ name: string;
3
+ frames: number[]; // Array of frame indices via flattened grid (0 to cols*rows - 1)
4
+ fps: number;
5
+ };
6
+
7
+ export class SpriteAnimator {
8
+ public columns: number = 1;
9
+ public rows: number = 1;
10
+ public atlasFrames?: { x: number; y: number; w: number; h: number }[];
11
+ public atlasSize?: { w: number; h: number };
12
+
13
+ public flipX: boolean = false;
14
+
15
+ public animations = new Map<string, AnimationDefinition>();
16
+
17
+ public currentAnimation: string = "";
18
+ public currentFrameIndex: number = 0; // The active frame index within the mapped animation's frames array
19
+ public isPlaying: boolean = true;
20
+ public timeAccumulator: number = 0;
21
+
22
+ constructor(columns: number = 1, rows: number = 1) {
23
+ this.columns = columns;
24
+ this.rows = rows;
25
+ }
26
+
27
+ addAnimation(name: string, frames: number[], fps: number) {
28
+ this.animations.set(name, { name, frames, fps });
29
+ if (!this.currentAnimation) {
30
+ this.play(name);
31
+ }
32
+ }
33
+
34
+ play(name: string) {
35
+ if (this.currentAnimation !== name) {
36
+ this.currentAnimation = name;
37
+ this.currentFrameIndex = 0;
38
+ this.timeAccumulator = 0;
39
+ }
40
+ this.isPlaying = true;
41
+ }
42
+ }
@@ -0,0 +1,26 @@
1
+ import {
2
+ DEFAULT_SPRITE_FS,
3
+ DEFAULT_SPRITE_VS,
4
+ Material,
5
+ } from "../renderer/Material";
6
+
7
+ export class SpriteRenderer {
8
+ public texture?: WebGLTexture;
9
+ public imageSrc: string;
10
+ public color: Float32Array = new Float32Array([1, 1, 1, 1]); // Tint
11
+
12
+ // UV mapping for sprite sheets
13
+ public uvOffset: Float32Array = new Float32Array([0, 0]);
14
+ public uvScale: Float32Array = new Float32Array([1, 1]);
15
+
16
+ public material: Material;
17
+
18
+ // WebGL specific tracking
19
+ public _loaded: boolean = false;
20
+
21
+ constructor(imageSrc: string, material?: Material) {
22
+ this.imageSrc = imageSrc;
23
+ this.material =
24
+ material || new Material(DEFAULT_SPRITE_VS, DEFAULT_SPRITE_FS);
25
+ }
26
+ }
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { quat, vec3 } from "gl-matrix";
3
+ import { Transform } from "./Transform";
4
+
5
+ describe("Transform Hierarchy", () => {
6
+ test("Parent child global position transformation", () => {
7
+ const root = new Transform();
8
+ vec3.set(root.localPosition, 10, 0, 0);
9
+ root.updateMatrix();
10
+
11
+ const child = new Transform();
12
+ child.setParent(root);
13
+ vec3.set(child.localPosition, 5, 0, 0);
14
+
15
+ // Update root which propagates to children
16
+ root.updateMatrix();
17
+
18
+ expect(root.position[0]).toBe(10);
19
+ expect(child.position[0]).toBe(15);
20
+ });
21
+
22
+ test("Parent child global rotation transformation", () => {
23
+ const root = new Transform();
24
+ // Rotate 90 degrees on Y axis
25
+ quat.setAxisAngle(root.localRotation, [0, 1, 0], Math.PI / 2);
26
+
27
+ const child = new Transform();
28
+ child.setParent(root);
29
+ // Move child by 5 units on X in local space
30
+ vec3.set(child.localPosition, 5, 0, 0);
31
+
32
+ root.updateMatrix();
33
+
34
+ // In global space, 5 units on X rotated 90 deg around Y should result in 5 units on -Z
35
+ expect(child.position[0]).toBeCloseTo(0, 5);
36
+ expect(child.position[1]).toBeCloseTo(0, 5);
37
+ expect(child.position[2]).toBeCloseTo(-5, 5);
38
+ });
39
+ });
@@ -0,0 +1,54 @@
1
+ import { mat4, quat, vec3 } from "gl-matrix";
2
+
3
+ export class Transform {
4
+ public position: vec3 = vec3.create();
5
+ public rotation: quat = quat.create();
6
+ public scale: vec3 = vec3.fromValues(1, 1, 1);
7
+
8
+ public localPosition: vec3 = vec3.create();
9
+ public localRotation: quat = quat.create();
10
+ public localScale: vec3 = vec3.fromValues(1, 1, 1);
11
+
12
+ public matrix: mat4 = mat4.create();
13
+ public localMatrix: mat4 = mat4.create();
14
+
15
+ public parent: Transform | null = null;
16
+ public children: Set<Transform> = new Set();
17
+
18
+ setParent(newParent: Transform | null) {
19
+ if (this.parent === newParent) return;
20
+
21
+ if (this.parent) {
22
+ this.parent.children.delete(this);
23
+ }
24
+
25
+ this.parent = newParent;
26
+
27
+ if (this.parent) {
28
+ this.parent.children.add(this);
29
+ }
30
+ }
31
+
32
+ updateMatrix() {
33
+ mat4.fromRotationTranslationScale(
34
+ this.localMatrix,
35
+ this.localRotation,
36
+ this.localPosition,
37
+ this.localScale,
38
+ );
39
+
40
+ if (this.parent) {
41
+ mat4.multiply(this.matrix, this.parent.matrix, this.localMatrix);
42
+ } else {
43
+ mat4.copy(this.matrix, this.localMatrix);
44
+ }
45
+
46
+ mat4.getTranslation(this.position, this.matrix);
47
+ mat4.getRotation(this.rotation, this.matrix);
48
+ mat4.getScaling(this.scale, this.matrix);
49
+
50
+ for (const child of this.children) {
51
+ child.updateMatrix();
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,123 @@
1
+ export class TextureAsset {
2
+ public image: HTMLImageElement;
3
+ public glTexture?: WebGLTexture;
4
+ public loaded: boolean = false;
5
+
6
+ constructor(public url: string) {
7
+ this.image = new Image();
8
+ this.image.crossOrigin = "anonymous";
9
+ }
10
+
11
+ async load(): Promise<this> {
12
+ return new Promise((resolve, reject) => {
13
+ if (this.loaded) {
14
+ return resolve(this);
15
+ }
16
+ this.image.onload = () => {
17
+ this.loaded = true;
18
+ resolve(this);
19
+ };
20
+ this.image.onerror = reject;
21
+ this.image.src = this.url;
22
+ });
23
+ }
24
+ }
25
+
26
+ export interface BMFontChar {
27
+ id: number;
28
+ x: number;
29
+ y: number;
30
+ width: number;
31
+ height: number;
32
+ xoffset: number;
33
+ yoffset: number;
34
+ xadvance: number;
35
+ }
36
+
37
+ export interface BMFontMetrics {
38
+ textureWidth: number;
39
+ textureHeight: number;
40
+ lineHeight: number;
41
+ base: number;
42
+ chars: Map<number, BMFontChar>;
43
+ textureAsset: TextureAsset;
44
+ }
45
+
46
+ export class AssetManagerImpl {
47
+ public textures = new Map<string, TextureAsset>();
48
+ public fonts = new Map<string, BMFontMetrics>();
49
+
50
+ public loadTexture(url: string): TextureAsset {
51
+ if (this.textures.has(url)) {
52
+ const asset = this.textures.get(url)!;
53
+ asset.load().catch(console.error);
54
+ return asset;
55
+ }
56
+ const tex = new TextureAsset(url);
57
+ this.textures.set(url, tex);
58
+ tex.load().catch(console.error);
59
+ return tex;
60
+ }
61
+
62
+ public getTexture(url: string): TextureAsset | undefined {
63
+ return this.textures.get(url);
64
+ }
65
+
66
+ public async loadBMFont(
67
+ fontId: string,
68
+ xmlUrl: string,
69
+ textureUrl: string,
70
+ ): Promise<BMFontMetrics> {
71
+ let metrics = this.fonts.get(fontId);
72
+ if (metrics) return metrics;
73
+
74
+ const textureAsset = this.loadTexture(textureUrl);
75
+ await textureAsset.load();
76
+
77
+ const response = await fetch(xmlUrl);
78
+ const xmlText = await response.text();
79
+
80
+ const parser = new DOMParser();
81
+ const doc = parser.parseFromString(xmlText, "text/xml");
82
+
83
+ const common = doc.querySelector("common")!;
84
+ const textureWidth = parseInt(common.getAttribute("scaleW") || "256", 10);
85
+ const textureHeight = parseInt(common.getAttribute("scaleH") || "256", 10);
86
+ const lineHeight = parseInt(common.getAttribute("lineHeight") || "32", 10);
87
+ const base = parseInt(common.getAttribute("base") || "32", 10);
88
+
89
+ const charsMap = new Map<number, BMFontChar>();
90
+ const chars = doc.querySelectorAll("char");
91
+ for (let i = 0; i < chars.length; i++) {
92
+ const c = chars.item(i);
93
+ charsMap.set(parseInt(c.getAttribute("id")!, 10), {
94
+ id: parseInt(c.getAttribute("id")!, 10),
95
+ x: parseInt(c.getAttribute("x")!, 10),
96
+ y: parseInt(c.getAttribute("y")!, 10),
97
+ width: parseInt(c.getAttribute("width")!, 10),
98
+ height: parseInt(c.getAttribute("height")!, 10),
99
+ xoffset: parseInt(c.getAttribute("xoffset")!, 10),
100
+ yoffset: parseInt(c.getAttribute("yoffset")!, 10),
101
+ xadvance: parseInt(c.getAttribute("xadvance")!, 10),
102
+ });
103
+ }
104
+
105
+ metrics = {
106
+ textureWidth,
107
+ textureHeight,
108
+ lineHeight,
109
+ base,
110
+ chars: charsMap,
111
+ textureAsset,
112
+ };
113
+ this.fonts.set(fontId, metrics);
114
+ return metrics;
115
+ }
116
+
117
+ public getFont(fontId: string): BMFontMetrics | undefined {
118
+ return this.fonts.get(fontId);
119
+ }
120
+ }
121
+
122
+ export const GlobalAssets = new AssetManagerImpl();
123
+ export class AssetManager extends AssetManagerImpl {}
@@ -0,0 +1,67 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+ import { Input } from "./Input";
3
+
4
+ describe("Input Manager", () => {
5
+ let listeners: Record<string, (...args: any[]) => void> = {};
6
+
7
+ beforeEach(() => {
8
+ listeners = {};
9
+ // Mock Window
10
+ globalThis.window = {
11
+ addEventListener: (type: string, fn: (...args: any[]) => void) => {
12
+ listeners[type] = fn;
13
+ },
14
+ } as any;
15
+ (Input as any)._initialized = false;
16
+ Input.initialize();
17
+ });
18
+
19
+ test("Action Mapping and binding", () => {
20
+ Input.bind("Jump", ["Space"]);
21
+ // Simulate keydown
22
+ (Input as any)._keys.add("Space");
23
+
24
+ expect(Input.isActionPressed("Jump")).toBe(true);
25
+ });
26
+
27
+ test("Edge detection logic", () => {
28
+ Input.bind("Fire", ["Enter"]);
29
+ (Input as any)._keys.add("Enter");
30
+
31
+ expect(Input.isActionJustPressed("Fire")).toBe(true);
32
+ expect(Input.isActionPressed("Fire")).toBe(true);
33
+
34
+ Input.update(); // End of Frame 1
35
+ expect(Input.isActionJustPressed("Fire")).toBe(false);
36
+ expect(Input.isActionPressed("Fire")).toBe(true);
37
+
38
+ (Input as any)._keys.delete("Enter");
39
+
40
+ expect(Input.isActionJustReleased("Fire")).toBe(true);
41
+ expect(Input.isActionPressed("Fire")).toBe(false);
42
+
43
+ Input.update(); // End of Frame 2
44
+ expect(Input.isActionJustReleased("Fire")).toBe(false);
45
+ });
46
+
47
+ test("Mouse interactions", () => {
48
+ listeners.mousemove({ clientX: 100, clientY: 200 });
49
+ expect(Input.getMousePosition().x).toBe(100);
50
+ expect(Input.getMousePosition().y).toBe(200);
51
+
52
+ listeners.mousedown();
53
+ expect(Input.isMouseDown()).toBe(true);
54
+ expect(Input.isMouseJustPressed()).toBe(true);
55
+
56
+ Input.update();
57
+ expect(Input.isMouseDown()).toBe(true);
58
+ expect(Input.isMouseJustPressed()).toBe(false);
59
+
60
+ listeners.mouseup();
61
+ expect(Input.isMouseDown()).toBe(false);
62
+ expect(Input.isMouseJustReleased()).toBe(true);
63
+
64
+ Input.update();
65
+ expect(Input.isMouseJustReleased()).toBe(false);
66
+ });
67
+ });