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