@dryanovski/gamefoo 0.2.3 → 0.2.5

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 (84) hide show
  1. package/dist/core/animate.js +147 -0
  2. package/dist/core/asset.js +74 -0
  3. package/dist/core/behaviour.js +88 -0
  4. package/dist/core/behaviours/collidable.js +186 -0
  5. package/dist/core/behaviours/control.js +75 -0
  6. package/dist/core/behaviours/healtkit.js +153 -0
  7. package/dist/core/behaviours/sprite_render.js +193 -0
  8. package/dist/core/camera.js +134 -0
  9. package/dist/core/engine.d.ts +1 -1
  10. package/dist/core/engine.d.ts.map +1 -1
  11. package/dist/core/engine.js +527 -0
  12. package/dist/core/fonts/font_bitmap.js +205 -0
  13. package/dist/core/fonts/font_bitmap_prebuild.js +137 -0
  14. package/dist/core/fonts/internal/font_3x5.js +169 -0
  15. package/dist/core/fonts/internal/font_4x6.js +171 -0
  16. package/dist/core/fonts/internal/font_5x5.js +129 -0
  17. package/dist/core/fonts/internal/font_6x8.js +171 -0
  18. package/dist/core/fonts/internal/font_8x13.js +171 -0
  19. package/dist/core/fonts/internal/font_8x8.js +171 -0
  20. package/dist/core/game_object_register.js +134 -0
  21. package/dist/core/input.js +170 -0
  22. package/dist/core/sprite.js +222 -0
  23. package/dist/core/utils/perlin_noise.js +183 -0
  24. package/dist/core/world.js +304 -0
  25. package/dist/debug/monitor.js +47 -0
  26. package/dist/decorators/index.js +1 -0
  27. package/dist/decorators/log.js +42 -0
  28. package/dist/entities/dynamic_entity.js +99 -0
  29. package/dist/entities/entity.js +283 -0
  30. package/dist/entities/player.js +93 -0
  31. package/dist/entities/text.js +62 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +47 -29
  35. package/dist/subsystems/camera_system.d.ts +2 -2
  36. package/dist/subsystems/camera_system.d.ts.map +1 -1
  37. package/dist/subsystems/camera_system.js +41 -0
  38. package/dist/subsystems/collision_system.js +17 -0
  39. package/dist/subsystems/monitor_system.js +20 -0
  40. package/dist/subsystems/object_system.d.ts +1 -1
  41. package/dist/subsystems/object_system.d.ts.map +1 -1
  42. package/dist/subsystems/object_system.js +29 -0
  43. package/dist/subsystems/types.d.ts +1 -1
  44. package/dist/subsystems/types.d.ts.map +1 -1
  45. package/dist/subsystems/types.js +0 -0
  46. package/dist/types.js +10 -0
  47. package/package.json +17 -6
  48. package/src/core/animate.ts +159 -0
  49. package/src/core/asset.ts +76 -0
  50. package/src/core/behaviour.ts +145 -0
  51. package/src/core/behaviours/collidable.ts +296 -0
  52. package/src/core/behaviours/control.ts +80 -0
  53. package/src/core/behaviours/healtkit.ts +166 -0
  54. package/src/core/behaviours/sprite_render.ts +216 -0
  55. package/src/core/camera.ts +145 -0
  56. package/src/core/engine.ts +607 -0
  57. package/src/core/fonts/font_bitmap.ts +232 -0
  58. package/src/core/fonts/font_bitmap_prebuild.ts +141 -0
  59. package/src/core/fonts/internal/font_3x5.ts +178 -0
  60. package/src/core/fonts/internal/font_4x6.ts +180 -0
  61. package/src/core/fonts/internal/font_5x5.ts +137 -0
  62. package/src/core/fonts/internal/font_6x8.ts +180 -0
  63. package/src/core/fonts/internal/font_8x13.ts +180 -0
  64. package/src/core/fonts/internal/font_8x8.ts +180 -0
  65. package/src/core/game_object_register.ts +146 -0
  66. package/src/core/input.ts +182 -0
  67. package/src/core/sprite.ts +339 -0
  68. package/src/core/utils/perlin_noise.ts +196 -0
  69. package/src/core/world.ts +331 -0
  70. package/src/debug/monitor.ts +60 -0
  71. package/src/decorators/index.ts +1 -0
  72. package/src/decorators/log.ts +45 -0
  73. package/src/entities/dynamic_entity.ts +106 -0
  74. package/src/entities/entity.ts +322 -0
  75. package/src/entities/player.ts +99 -0
  76. package/src/entities/text.ts +72 -0
  77. package/src/index.ts +51 -0
  78. package/src/subsystems/camera_system.ts +52 -0
  79. package/src/subsystems/collision_system.ts +21 -0
  80. package/src/subsystems/monitor_system.ts +26 -0
  81. package/src/subsystems/object_system.ts +37 -0
  82. package/src/subsystems/types.ts +46 -0
  83. package/src/types.ts +178 -0
  84. package/dist/index.js.map +0 -9
@@ -0,0 +1,331 @@
1
+ import type { WorldBounds } from "../types";
2
+ import type { Collidable } from "./behaviours/collidable";
3
+
4
+ /**
5
+ * Spatial collision-detection world.
6
+ *
7
+ * `World` maintains a set of {@link Collidable} behaviours and, each
8
+ * frame, performs an **O(n^2)** broad-phase + narrow-phase pass via
9
+ * {@link World.detect}. It supports:
10
+ *
11
+ * - **Layer filtering** — only colliders on the same layer are tested.
12
+ * - **Tag-based interest** — a collider only receives callbacks for
13
+ * tags it has opted into via `collidesWith`.
14
+ * - **Shape combinations** — AABB vs AABB, circle vs circle, and
15
+ * circle vs AABB.
16
+ * - **Solid overlap resolution** — when both colliders are marked
17
+ * `solid`, entities are pushed apart along the axis of least
18
+ * penetration.
19
+ * - **Fixed bodies** — colliders flagged `fixed` are immovable; the
20
+ * other body absorbs the full push.
21
+ *
22
+ * @category Core
23
+ * @since 0.1.0
24
+ *
25
+ * @example Registering colliders
26
+ * ```ts
27
+ * const world = new World();
28
+ *
29
+ * const collidable = new Collidable(entity, world, {
30
+ * shape: { type: "aabb", width: 32, height: 32 },
31
+ * layer: 0,
32
+ * tags: new Set(["enemy"]),
33
+ * solid: true,
34
+ * collidesWith: new Set(["player", "bullet"]),
35
+ * });
36
+ *
37
+ * entity.attachBehaviour(collidable); // calls world.register internally
38
+ * ```
39
+ *
40
+ * @example Running detection manually
41
+ * ```ts
42
+ * world.detect(); // typically called by Engine.update each frame
43
+ * ```
44
+ *
45
+ * @see {@link Collidable} — the behaviour that plugs into this world
46
+ * @see {@link Engine} — calls {@link World.detect} every frame
47
+ */
48
+ export default class World {
49
+ /**
50
+ * The live set of all registered {@link Collidable} behaviours.
51
+ */
52
+ private colliders: Set<Collidable> = new Set();
53
+
54
+ /**
55
+ * Adds a collider to the world so it participates in future
56
+ * {@link World.detect} passes.
57
+ *
58
+ * Called automatically by {@link Collidable.onAttach}.
59
+ *
60
+ * @param collider - The collidable behaviour to register.
61
+ */
62
+ register(collider: Collidable): void {
63
+ this.colliders.add(collider);
64
+ }
65
+
66
+ /**
67
+ * Removes a collider from the world.
68
+ *
69
+ * Called automatically by {@link Collidable.onDetach}.
70
+ *
71
+ * @param collider - The collidable behaviour to remove.
72
+ */
73
+ unregister(collider: Collidable): void {
74
+ this.colliders.delete(collider);
75
+ }
76
+
77
+ /**
78
+ * Runs one full collision-detection pass over every registered
79
+ * collider.
80
+ *
81
+ * **Algorithm:**
82
+ *
83
+ * 1. Iterate all unique pairs `(i, j)` where `i < j`.
84
+ * 2. Skip disabled colliders or mismatched layers.
85
+ * 3. Check tag interest in both directions.
86
+ * 4. Compute world bounds and test intersection.
87
+ * 5. If both are `solid`, resolve the overlap.
88
+ * 6. Fire `onCollision` callbacks on interested sides.
89
+ *
90
+ * @since 0.1.0
91
+ */
92
+ detect(): void {
93
+ /**
94
+ * Note: this naive O(n^2) approach is fine for small numbers of colliders
95
+ * (e.g. <100) but will degrade rapidly as that grows. For larger games,
96
+ * consider implementing spatial partitioning (e.g. quad-trees) to reduce
97
+ * the number of pairwise checks.
98
+ * In the meantime, users can mitigate performance issues by carefully
99
+ * managing which colliders are active and using layers/tags to minimize
100
+ * unnecessary checks.
101
+ * This method is intentionally straightforward for clarity and ease of
102
+ * extension (e.g. adding new shapes or filters) in the early stages of
103
+ * development.
104
+ *
105
+ * Future optimizations could include:
106
+ * - Spatial partitioning (quad-trees, grids)
107
+ * - Sweep and prune (sorting by axis)
108
+ * - Early-out checks (e.g. bounding circles)
109
+ * - Parallel processing (Web Workers)
110
+ * - Configurable broad-phase strategies
111
+ * - Caching world bounds and only updating when necessary
112
+ * - Object pooling for collision data structures
113
+ * - Profiling and optimizing hot paths (e.g. intersection tests)
114
+ * - Allowing users to provide custom collision filters or callbacks
115
+ * - Supporting more complex shapes (polygons, capsules) with appropriate tests
116
+ * - Providing debug visualization tools to help users understand collisions
117
+ * - Documenting best practices for performance (e.g. using layers/tags effectively)
118
+ * - Providing warnings or profiling tools when performance degrades due to too many colliders
119
+ * - etc.
120
+ */
121
+ if (this.colliders.size === 0) return;
122
+
123
+ const list = Array.from(this.colliders);
124
+ const len = list.length;
125
+
126
+ for (let i = 0; i < len; i++) {
127
+ const obj = list[i];
128
+ if (!obj?.enabled) continue;
129
+
130
+ for (let j = i + 1; j < len; j++) {
131
+ const other = list[j];
132
+ if (!other?.enabled) continue;
133
+
134
+ if (obj.layer !== other.layer) continue;
135
+
136
+ const objWantOther = this.tagsOverlap(obj.collidesWith, other.tags);
137
+ const otherWantObj = this.tagsOverlap(other.collidesWith, obj.tags);
138
+
139
+ const boundsObj = obj.getWorldBounds();
140
+ const boundsOther = other.getWorldBounds();
141
+
142
+ if (!this.intersects(obj, boundsObj, other, boundsOther)) continue;
143
+
144
+ if (obj.solid && other.solid) {
145
+ this.resolveOverlap(obj, boundsObj, other, boundsOther);
146
+ }
147
+
148
+ if (objWantOther && obj.onCollision) {
149
+ obj.onCollision({
150
+ self: obj.getOwner(),
151
+ other: other.getOwner(),
152
+ selfTags: obj.tags,
153
+ otherTags: other.tags,
154
+ });
155
+ }
156
+
157
+ if (otherWantObj && other.onCollision) {
158
+ other.onCollision({
159
+ self: other.getOwner(),
160
+ other: obj.getOwner(),
161
+ selfTags: other.tags,
162
+ otherTags: obj.tags,
163
+ });
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Returns `true` if any tag in `wants` exists in `has`.
171
+ *
172
+ * @param wants - Tags the collider is interested in.
173
+ * @param has - Tags the other collider owns.
174
+ * @returns Whether at least one tag overlaps.
175
+ *
176
+ * @internal
177
+ */
178
+ private tagsOverlap(wants: Set<string>, has: Set<string>): boolean {
179
+ for (const tag of wants) {
180
+ if (has.has(tag)) return true;
181
+ }
182
+ return false;
183
+ }
184
+
185
+ /**
186
+ * Dispatches to the correct narrow-phase test based on collider
187
+ * shape types.
188
+ *
189
+ * Supports AABB-vs-AABB, circle-vs-circle, and circle-vs-AABB.
190
+ *
191
+ * @param a - First collidable.
192
+ * @param boundsA - World bounds of `a`.
193
+ * @param b - Second collidable.
194
+ * @param boundsB - World bounds of `b`.
195
+ * @returns `true` if the two shapes overlap.
196
+ *
197
+ * @internal
198
+ */
199
+ private intersects(a: Collidable, boundsA: WorldBounds, b: Collidable, boundsB: WorldBounds): boolean {
200
+ const shapeA = a.shape;
201
+ const shapeB = b.shape;
202
+
203
+ if (shapeA.type === "aabb" && shapeB.type === "aabb") {
204
+ return this.aabbVSAabb(boundsA, boundsB);
205
+ }
206
+
207
+ if (shapeA.type === "circle" && shapeB.type === "circle") {
208
+ return this.circleVSCircle(a, boundsA, b, boundsB);
209
+ }
210
+
211
+ const [circle, circleBounds, rect] = shapeA.type === "circle" ? [a, boundsA, boundsB] : [b, boundsB, boundsA];
212
+
213
+ return this.circleVSAAabb(circle, circleBounds, rect);
214
+ }
215
+
216
+ /**
217
+ * AABB-vs-AABB overlap test.
218
+ *
219
+ * @param a - First bounding rectangle.
220
+ * @param b - Second bounding rectangle.
221
+ * @returns `true` if the rectangles overlap.
222
+ *
223
+ * @internal
224
+ */
225
+ private aabbVSAabb(a: WorldBounds, b: WorldBounds): boolean {
226
+ return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
227
+ }
228
+
229
+ /**
230
+ * Circle-vs-circle overlap test using squared-distance comparison
231
+ * (avoids `Math.sqrt`).
232
+ *
233
+ * @param a - First collidable (must have `circle` shape).
234
+ * @param boundsA - World bounds of `a`.
235
+ * @param b - Second collidable (must have `circle` shape).
236
+ * @param boundsB - World bounds of `b`.
237
+ * @returns `true` if the circles overlap.
238
+ *
239
+ * @internal
240
+ */
241
+ private circleVSCircle(a: Collidable, boundsA: WorldBounds, b: Collidable, boundsB: WorldBounds): boolean {
242
+ if (a.shape.type !== "circle" || b.shape.type !== "circle") return false;
243
+
244
+ const cx1 = boundsA.x + a.shape.radius;
245
+ const cy1 = boundsA.y + a.shape.radius;
246
+ const cx2 = boundsB.x + b.shape.radius;
247
+ const cy2 = boundsB.y + b.shape.radius;
248
+
249
+ const dx = cx2 - cx1;
250
+ const dy = cy2 - cy1;
251
+ const distSq = dx * dx + dy * dy;
252
+ const radSum = a.shape.radius + b.shape.radius;
253
+
254
+ return distSq <= radSum * radSum;
255
+ }
256
+
257
+ /**
258
+ * Circle-vs-AABB overlap test. Finds the closest point on the
259
+ * rectangle to the circle centre and checks the squared distance.
260
+ *
261
+ * @param circle - The collidable with a `circle` shape.
262
+ * @param circleBounds - World bounds of the circle collider.
263
+ * @param rect - World bounds of the AABB collider.
264
+ * @returns `true` if the circle and rectangle overlap.
265
+ *
266
+ * @internal
267
+ */
268
+ private circleVSAAabb(circle: Collidable, circleBounds: WorldBounds, rect: WorldBounds): boolean {
269
+ if (circle.shape.type !== "circle") return false;
270
+
271
+ const cx = circleBounds.x + circle.shape.radius;
272
+ const cy = circleBounds.y + circle.shape.radius;
273
+
274
+ const closestX = Math.max(rect.x, Math.min(cx, rect.x + rect.width));
275
+ const closestY = Math.max(rect.y, Math.min(cy, rect.y + rect.height));
276
+
277
+ const dx = cx - closestX;
278
+ const dy = cy - closestY;
279
+
280
+ return dx * dx + dy * dy <= circle.shape.radius * circle.shape.radius;
281
+ }
282
+
283
+ /**
284
+ * Resolves positional overlap between two solid colliders by pushing
285
+ * their owning entities apart along the axis of minimum penetration.
286
+ *
287
+ * Respects the `fixed` flag: if one collider is fixed the other
288
+ * absorbs the full displacement; if both are fixed, no resolution
289
+ * occurs.
290
+ *
291
+ * @param a - First collidable.
292
+ * @param boundsA - World bounds of `a`.
293
+ * @param b - Second collidable.
294
+ * @param boundsB - World bounds of `b`.
295
+ *
296
+ * @internal
297
+ */
298
+ private resolveOverlap(a: Collidable, boundsA: WorldBounds, b: Collidable, boundsB: WorldBounds): void {
299
+ const overlapX = Math.min(boundsA.x + boundsA.width - boundsB.x, boundsB.x + boundsB.width - boundsA.x);
300
+ const overlapY = Math.min(boundsA.y + boundsA.height - boundsB.y, boundsB.y + boundsB.height - boundsA.y);
301
+
302
+ let pushX = 0;
303
+ let pushY = 0;
304
+
305
+ if (overlapX < overlapY) {
306
+ pushX = boundsA.x < boundsB.x ? -overlapX : overlapX;
307
+ } else {
308
+ pushY = boundsA.y < boundsB.y ? -overlapY : overlapY;
309
+ }
310
+
311
+ const ownerA = a.getOwner();
312
+ const ownerB = b.getOwner();
313
+
314
+ if (a.fixed && b.fixed) {
315
+ return;
316
+ }
317
+
318
+ if (a.fixed) {
319
+ ownerB.x -= pushX;
320
+ ownerB.y -= pushY;
321
+ } else if (b.fixed) {
322
+ ownerA.x += pushX;
323
+ ownerA.y += pushY;
324
+ } else {
325
+ ownerA.x += pushX / 2;
326
+ ownerA.y += pushY / 2;
327
+ ownerB.x -= pushX / 2;
328
+ ownerB.y -= pushY / 2;
329
+ }
330
+ }
331
+ }
@@ -0,0 +1,60 @@
1
+ import FontBitmap from "../core/fonts/font_bitmap";
2
+
3
+ const font = new FontBitmap("5x5");
4
+
5
+ export default class Monitor {
6
+ private fps: number = 0;
7
+ private timer: number = 0;
8
+ private frameCount: number = 0;
9
+ private memory: number = 0;
10
+
11
+ private frames: number[] = [0];
12
+
13
+ public x: number = 8;
14
+ public y: number = 8;
15
+
16
+ update(delta: number): void {
17
+ this.frameCount++;
18
+ this.timer += delta;
19
+
20
+ if (this.timer >= 1.0) {
21
+ this.fps = this.frameCount / this.timer;
22
+ this.frameCount = 0;
23
+ this.timer = 0;
24
+
25
+ this.frames.push(this.fps);
26
+ if (this.frames.length > 60) {
27
+ this.frames.shift();
28
+ }
29
+ }
30
+
31
+ if ((performance as any).memory) {
32
+ const mem = (performance as any).memory;
33
+ this.memory = mem.usedJSHeapSize / 1048576;
34
+ }
35
+ }
36
+
37
+ render(ctx: CanvasRenderingContext2D): void {
38
+ ctx.save();
39
+
40
+ ctx.fillStyle = "#fff";
41
+ ctx.strokeStyle = "#fff";
42
+ font.renderText(`FPS: ${this.fps.toFixed(1)}`, this.x, this.y, ctx);
43
+
44
+ if (this.memory) {
45
+ font.renderText(`MEM: ${this.memory.toFixed(1)} MB`, this.x, this.y + font.height + 3, ctx);
46
+ }
47
+
48
+ if (this.frames.length >= 1) {
49
+ ctx.beginPath();
50
+ for (let i = 0; i < this.frames.length; i++) {
51
+ const x = this.x + i;
52
+ const y = this.x + font.height * 2 + 80 - (this.frames[i] ?? 0);
53
+ ctx.lineTo(x, y);
54
+ }
55
+ ctx.stroke();
56
+ }
57
+
58
+ ctx.restore();
59
+ }
60
+ }
@@ -0,0 +1 @@
1
+ export * from "./log";
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A method decorator that logs the method name, arguments, and return value.
3
+ *
4
+ * Uses the legacy (experimental) decorator protocol for compatibility with
5
+ * Bun's browser bundler.
6
+ *
7
+ * @category Decorators
8
+ * @since 0.2.0
9
+ *
10
+ * @param target - The prototype of the class (instance method) or the constructor (static method).
11
+ * @param propertyKey - The name of the decorated method.
12
+ * @param descriptor - The property descriptor for the method.
13
+ *
14
+ * @returns The modified property descriptor with logging behaviour.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * class Example {
19
+ * @log
20
+ * myMethod(arg1: string, arg2: number) {
21
+ * return `${arg1} - ${arg2}`;
22
+ * }
23
+ * }
24
+ *
25
+ * const example = new Example();
26
+ * example.myMethod('test', 42);
27
+ * Output:
28
+ * ▶ myMethod(["test", 42])
29
+ * ◀ myMethod → "test - 42"
30
+ * ```
31
+ */
32
+ export function log(target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor {
33
+ const originalMethod = descriptor.value;
34
+
35
+ const prefix = `${target.constructor.name || "anonymous"}.${String(propertyKey)}`;
36
+
37
+ descriptor.value = function (this: unknown, ...args: unknown[]) {
38
+ console.log(`▶ ${prefix}(${JSON.stringify(args)})`);
39
+ const result = originalMethod.apply(this, args);
40
+ console.log(`◀ ${prefix} →`, result);
41
+ return result;
42
+ };
43
+
44
+ return descriptor;
45
+ }
@@ -0,0 +1,106 @@
1
+ import type { Vector2 } from "../types";
2
+ import Entity from "./entity";
3
+
4
+ /**
5
+ * Abstract entity with built-in velocity and speed, suitable for any
6
+ * game object that moves (players, NPCs, projectiles, etc.).
7
+ *
8
+ * `DynamicEntity` extends {@link Entity} with a {@link Vector2}
9
+ * velocity and a scalar speed, plus getter/setter pairs for each.
10
+ * Subclasses are responsible for applying the velocity to position
11
+ * inside their {@link Entity.update | update} implementation.
12
+ *
13
+ * @category Entities
14
+ * @since 0.1.0
15
+ *
16
+ * @example Subclassing
17
+ * ```ts
18
+ * import { DynamicEntity } from "gamefoo";
19
+ *
20
+ * class Bullet extends DynamicEntity {
21
+ * constructor(x: number, y: number) {
22
+ * super("bullet", x, y, 4, 4);
23
+ * this.setSpeed(600);
24
+ * this.setVelocity({ x: 1, y: 0 });
25
+ * }
26
+ *
27
+ * update(dt: number) {
28
+ * this.x += this.velocity.x * this.speed * dt;
29
+ * this.y += this.velocity.y * this.speed * dt;
30
+ * }
31
+ *
32
+ * render(ctx: CanvasRenderingContext2D) {
33
+ * ctx.fillStyle = "#ff0";
34
+ * ctx.fillRect(this.x, this.y, 4, 4);
35
+ * }
36
+ * }
37
+ * ```
38
+ *
39
+ * @see {@link Entity} — parent class (identity, transform, behaviours)
40
+ * @see {@link Player} — concrete dynamic entity for the player
41
+ */
42
+ export default abstract class DynamicEntity extends Entity {
43
+ /**
44
+ * Directional velocity vector.
45
+ *
46
+ * Represents the normalised (or raw) direction of movement. Multiply
47
+ * by {@link DynamicEntity.speed | speed} and `deltaTime` to get the
48
+ * per-frame displacement.
49
+ *
50
+ * @defaultValue `{ x: 0, y: 0 }`
51
+ */
52
+ protected velocity: Vector2 = { x: 0, y: 0 };
53
+
54
+ /**
55
+ * Scalar movement speed in pixels per second.
56
+ *
57
+ * @defaultValue `0`
58
+ */
59
+ protected speed: number = 0;
60
+
61
+ /**
62
+ * Replaces the current velocity vector.
63
+ *
64
+ * @param velocity - The new velocity.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * entity.setVelocity({ x: -1, y: 0 }); // moving left
69
+ * ```
70
+ */
71
+ setVelocity(velocity: Vector2): void {
72
+ this.velocity = velocity;
73
+ }
74
+
75
+ /**
76
+ * Returns a **copy** of the current velocity vector.
77
+ *
78
+ * @returns A new {@link Vector2}.
79
+ */
80
+ getVelocity(): Vector2 {
81
+ return { ...this.velocity };
82
+ }
83
+
84
+ /**
85
+ * Sets the scalar movement speed.
86
+ *
87
+ * @param speed - Speed in pixels per second.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * entity.setSpeed(200);
92
+ * ```
93
+ */
94
+ setSpeed(speed: number): void {
95
+ this.speed = speed;
96
+ }
97
+
98
+ /**
99
+ * Returns the current movement speed.
100
+ *
101
+ * @returns Speed in pixels per second.
102
+ */
103
+ getSpeed(): number {
104
+ return this.speed;
105
+ }
106
+ }