@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,296 @@
1
+ import type Entity from "../../entities/entity";
2
+ import type { ColliderShape, CollisionInfo, GameObject, WorldBounds } from "../../types";
3
+ import { Behaviour } from "../behaviour";
4
+ import type World from "../world";
5
+
6
+ /**
7
+ * Options for constructing a {@link Collidable} behaviour.
8
+ *
9
+ * Every field except `shape` is optional and has a sensible default.
10
+ *
11
+ * @category Behaviours
12
+ * @since 0.1.0
13
+ *
14
+ * @example Minimal options
15
+ * ```ts
16
+ * const opts: CollidableOptions = {
17
+ * shape: { type: "aabb", width: 32, height: 32 },
18
+ * };
19
+ * ```
20
+ *
21
+ * @example Full options
22
+ * ```ts
23
+ * const opts: CollidableOptions = {
24
+ * shape: { type: "circle", radius: 16 },
25
+ * layer: 0,
26
+ * tags: new Set(["enemy"]),
27
+ * solid: true,
28
+ * fixed: false,
29
+ * collidesWith: new Set(["player", "bullet"]),
30
+ * onCollision: (info) => console.log("hit!", info.other.id),
31
+ * };
32
+ * ```
33
+ */
34
+ type CollidableOptions = {
35
+ /**
36
+ * The geometric shape used for intersection tests.
37
+ *
38
+ * @see {@link ColliderShape}
39
+ */
40
+ shape: ColliderShape;
41
+
42
+ /**
43
+ * Collision layer index. Only colliders on the **same** layer are
44
+ * tested against each other.
45
+ *
46
+ * @defaultValue `0`
47
+ */
48
+ layer?: number;
49
+
50
+ /**
51
+ * Tags that identify *this* collider (e.g. `"player"`, `"bullet"`).
52
+ *
53
+ * @defaultValue empty `Set`
54
+ */
55
+ tags?: Set<string>;
56
+
57
+ /**
58
+ * Tags this collider is **interested in**. The `onCollision` callback
59
+ * only fires when the other collider has at least one matching tag.
60
+ *
61
+ * @defaultValue empty `Set`
62
+ */
63
+ collidesWith?: Set<string>;
64
+
65
+ /**
66
+ * Whether overlap resolution should be applied when this collider
67
+ * intersects another solid collider.
68
+ *
69
+ * @defaultValue `false`
70
+ */
71
+ solid?: boolean;
72
+
73
+ /**
74
+ * If `true`, this collider is treated as immovable during overlap
75
+ * resolution — the other entity absorbs the full displacement.
76
+ *
77
+ * @defaultValue `false`
78
+ */
79
+ fixed?: boolean;
80
+
81
+ /**
82
+ * Callback invoked when a collision with a tag-matched collider is
83
+ * detected.
84
+ *
85
+ * @param info - Details about the collision, including both entities
86
+ * and their tag sets.
87
+ *
88
+ * @see {@link CollisionInfo}
89
+ */
90
+ onCollision?: (info: CollisionInfo) => void;
91
+ };
92
+
93
+ /**
94
+ * Collision behaviour that can be attached to any {@link Entity}.
95
+ *
96
+ * When attached, the `Collidable` automatically registers itself with
97
+ * the engine's {@link World} (via {@link Collidable.onAttach}) and
98
+ * unregisters on detach. Each frame the `World` queries the collider's
99
+ * shape, bounds, and tags to determine intersections.
100
+ *
101
+ * @category Behaviours
102
+ * @since 0.1.0
103
+ *
104
+ * @example Creating and attaching a box collider
105
+ * ```ts
106
+ * import { Collidable, Entity, type CollisionInfo } from "gamefoo";
107
+ *
108
+ * const entity = new Enemy("goblin", 100, 200, 30, 30);
109
+ *
110
+ * entity.attachBehaviour(
111
+ * new Collidable(entity, engine.collisions, {
112
+ * shape: { type: "aabb", width: 30, height: 30 },
113
+ * layer: 0,
114
+ * tags: new Set(["enemy"]),
115
+ * solid: true,
116
+ * collidesWith: new Set(["player"]),
117
+ * onCollision: (info: CollisionInfo) => {
118
+ * console.log(`${info.self.id} hit ${info.other.id}`);
119
+ * },
120
+ * }),
121
+ * );
122
+ * ```
123
+ *
124
+ * @example Circle collider for a projectile
125
+ * ```ts
126
+ * entity.attachBehaviour(
127
+ * new Collidable(bullet, engine.collisions, {
128
+ * shape: { type: "circle", radius: 4 },
129
+ * tags: new Set(["bullet"]),
130
+ * collidesWith: new Set(["enemy"]),
131
+ * }),
132
+ * );
133
+ * ```
134
+ *
135
+ * @see {@link World} — the collision detection system
136
+ * @see {@link ColliderShape} — supported shape types
137
+ * @see {@link CollisionInfo} — payload delivered to callbacks
138
+ * @see {@link Behaviour} — abstract base class
139
+ */
140
+ export class Collidable extends Behaviour<GameObject> {
141
+ /** @inheritDoc */
142
+ readonly type = "collidable";
143
+
144
+ /**
145
+ * Geometric shape used for intersection tests.
146
+ *
147
+ * @see {@link ColliderShape}
148
+ */
149
+ public shape: ColliderShape;
150
+
151
+ /**
152
+ * Collision layer. Only colliders sharing the same layer value are
153
+ * tested.
154
+ *
155
+ * @defaultValue `0`
156
+ */
157
+ public layer: number = 0;
158
+
159
+ /**
160
+ * Tags identifying this collider (e.g. `"player"`, `"enemy"`).
161
+ *
162
+ * @defaultValue empty `Set`
163
+ */
164
+ public tags: Set<string> = new Set();
165
+
166
+ /**
167
+ * Tags this collider wants to be notified about.
168
+ *
169
+ * @defaultValue empty `Set`
170
+ */
171
+ public collidesWith: Set<string> = new Set();
172
+
173
+ /**
174
+ * Whether this collider participates in overlap resolution.
175
+ *
176
+ * @defaultValue `false`
177
+ */
178
+ public solid: boolean = false;
179
+
180
+ /**
181
+ * Whether the owning entity is immovable during overlap resolution.
182
+ *
183
+ * @defaultValue `false`
184
+ */
185
+ public fixed: boolean = false;
186
+
187
+ /**
188
+ * User-supplied callback invoked when a tag-matched collision is
189
+ * detected.
190
+ */
191
+ public onCollision: (info: CollisionInfo) => void;
192
+
193
+ /** Reference to the {@link World} this collider is registered with. */
194
+ private world: World;
195
+
196
+ /**
197
+ * Creates a new collidable behaviour.
198
+ *
199
+ * @param owner - The game object entity that owns this collider.
200
+ * @param world - The collision {@link World} to register with.
201
+ * @param options - Configuration for shape, tags, solidity, and
202
+ * callbacks. See {@link CollidableOptions}.
203
+ */
204
+ constructor(owner: GameObject, world: World, options: CollidableOptions) {
205
+ super(owner);
206
+
207
+ this.world = world;
208
+
209
+ const size = owner.getSize();
210
+
211
+ this.shape = options.shape ?? {
212
+ type: "aabb",
213
+ width: size.width,
214
+ height: size.height,
215
+ };
216
+ this.layer = options.layer ?? 0;
217
+ this.tags = options.tags ?? new Set();
218
+ this.solid = options.solid ?? false;
219
+ this.fixed = options.fixed ?? false;
220
+ this.collidesWith = options.collidesWith ?? new Set();
221
+ this.onCollision = options.onCollision || (() => {});
222
+ }
223
+
224
+ /**
225
+ * No-op — collision logic lives in {@link World.detect}.
226
+ *
227
+ * @param _deltaTime - Unused.
228
+ */
229
+ update(_deltaTime: number): void {}
230
+
231
+ /**
232
+ * Lifecycle hook: registers this collider with the {@link World}
233
+ * when the behaviour is attached to an entity.
234
+ *
235
+ * @see {@link Behaviour.onAttach}
236
+ */
237
+ override onAttach(): void {
238
+ this.world.register(this);
239
+ }
240
+
241
+ /**
242
+ * Lifecycle hook: removes this collider from the {@link World}
243
+ * when the behaviour is detached.
244
+ *
245
+ * @see {@link Behaviour.onDetach}
246
+ */
247
+ override onDetach(): void {
248
+ this.world.unregister(this);
249
+ }
250
+
251
+ /**
252
+ * Returns the {@link Entity} that owns this behaviour.
253
+ *
254
+ * Used by the {@link World} to read and mutate entity position
255
+ * during overlap resolution.
256
+ *
257
+ * @returns The owning entity.
258
+ */
259
+ getOwner(): Entity {
260
+ return this.owner;
261
+ }
262
+
263
+ /**
264
+ * Computes this collider's axis-aligned bounding rectangle in
265
+ * world-space, accounting for the shape's optional offset.
266
+ *
267
+ * @returns A {@link WorldBounds} rectangle.
268
+ *
269
+ * @example
270
+ * ```ts
271
+ * const bounds = collidable.getWorldBounds();
272
+ * // { x: 100, y: 200, width: 30, height: 30 }
273
+ * ```
274
+ */
275
+ getWorldBounds(): WorldBounds {
276
+ const pos = this.owner.getPosition();
277
+ const offset = "offset" in this.shape && this.shape.offset ? this.shape.offset : { x: 0, y: 0 };
278
+
279
+ if (this.shape.type === "aabb") {
280
+ return {
281
+ x: pos.x + offset.x,
282
+ y: pos.y + offset.y,
283
+ width: this.shape.width,
284
+ height: this.shape.height,
285
+ };
286
+ }
287
+
288
+ const r = this.shape.radius;
289
+ return {
290
+ x: pos.x + offset.x - r,
291
+ y: pos.y + offset.y - r,
292
+ width: r * 2,
293
+ height: r * 2,
294
+ };
295
+ }
296
+ }
@@ -0,0 +1,80 @@
1
+ import type Entity from "../../entities/entity";
2
+ import { Behaviour } from "../behaviour";
3
+ import type Input from "../input";
4
+
5
+ /**
6
+ * Keyboard-driven movement behaviour for a {@link Entity}.
7
+ *
8
+ * `Control` reads the current keyboard state from an {@link Input}
9
+ * instance every frame and translates WASD / arrow-key presses into
10
+ * entity position changes. Diagonal movement is normalised so the
11
+ * entity moves at a consistent speed in all directions.
12
+ *
13
+ * @category Behaviours
14
+ * @since 0.1.0
15
+ *
16
+ * @example Attaching to a player
17
+ * ```ts
18
+ * import { Control, Input, Player } from "gamefoo";
19
+ *
20
+ * const input = new Input();
21
+ * const player = new Player("hero", 400, 300, 50, 50);
22
+ *
23
+ * player.attachBehaviour(new Control(player, input));
24
+ * ```
25
+ *
26
+ * @see {@link Input} — the polling input manager consumed by this behaviour
27
+ * @see {@link Behaviour} — abstract base class
28
+ */
29
+ export class Control extends Behaviour<Entity> {
30
+ /** @inheritDoc */
31
+ readonly type = "control";
32
+
33
+ /** The input manager to poll each frame. */
34
+ private input: Input;
35
+
36
+ /**
37
+ * Movement speed in pixels per second.
38
+ *
39
+ * @defaultValue `500`
40
+ */
41
+ private speed: number = 500;
42
+
43
+ /**
44
+ * Creates a new keyboard control behaviour.
45
+ *
46
+ * @param owner - The game object entity whose position will be updated.
47
+ * @param input - The {@link Input} instance to read key state from.
48
+ */
49
+ constructor(owner: Entity, input: Input) {
50
+ super(owner);
51
+ this.input = input;
52
+ }
53
+
54
+ /**
55
+ * Reads the current key state and moves the owner entity.
56
+ *
57
+ * Supported keys: `W` / `ArrowUp`, `S` / `ArrowDown`,
58
+ * `A` / `ArrowLeft`, `D` / `ArrowRight`.
59
+ *
60
+ * Diagonal input is normalised so the effective speed remains
61
+ * constant regardless of direction.
62
+ *
63
+ * @param deltaTime - Seconds elapsed since the previous frame.
64
+ */
65
+ update(deltaTime: number): void {
66
+ let dx = 0;
67
+ let dy = 0;
68
+
69
+ if (this.input.isKeyDown("a") || this.input.isKeyDown("arrowleft")) dx -= 1;
70
+ if (this.input.isKeyDown("d") || this.input.isKeyDown("arrowright")) dx += 1;
71
+ if (this.input.isKeyDown("w") || this.input.isKeyDown("arrowup")) dy -= 1;
72
+ if (this.input.isKeyDown("s") || this.input.isKeyDown("arrowdown")) dy += 1;
73
+
74
+ const len = Math.sqrt(dx * dx + dy * dy);
75
+ if (len > 0) {
76
+ this.owner.x += (dx / len) * this.speed * deltaTime;
77
+ this.owner.y += (dy / len) * this.speed * deltaTime;
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,166 @@
1
+ import type Entity from "../../entities/entity";
2
+ import { Behaviour } from "../behaviour";
3
+
4
+ /**
5
+ * Health-tracking behaviour for a {@link Entity}.
6
+ *
7
+ * `HealthKit` manages a current and maximum HP value, provides damage
8
+ * and healing methods, and exposes queries for health percentage and
9
+ * death state.
10
+ *
11
+ * @category Behaviours
12
+ * @since 0.1.0
13
+ *
14
+ * @example Attaching to a player
15
+ * ```ts
16
+ * import { HealthKit, Player } from "gamefoo";
17
+ *
18
+ * const player = new Player("hero", 400, 300, 50, 50);
19
+ * player.attachBehaviour(new HealthKit(player, 100));
20
+ *
21
+ * player.healthkit?.takeDamage(25);
22
+ * console.log(player.healthkit?.getHealth()); // 75
23
+ * console.log(player.healthkit?.getHealthPercent()); // 0.75
24
+ * ```
25
+ *
26
+ * @example Custom max HP
27
+ * ```ts
28
+ * const hk = new HealthKit(entity, 50, 200);
29
+ * // starts at 50 HP, max is 200
30
+ * hk.heal(999);
31
+ * console.log(hk.getHealth()); // 200 (clamped to max)
32
+ * ```
33
+ *
34
+ * @see {@link Behaviour} — abstract base class
35
+ * @see {@link Player} — has a convenience getter for this behaviour
36
+ */
37
+ export class HealthKit extends Behaviour<Entity> {
38
+ /** @inheritDoc */
39
+ readonly type = "healthkit";
40
+
41
+ /** Current health points. */
42
+ private health: number;
43
+
44
+ /** Maximum health points (healing cap). */
45
+ private maxHP: number;
46
+
47
+ /**
48
+ * Creates a new health behaviour.
49
+ *
50
+ * @param owner - The entity this behaviour is attached to.
51
+ * @param health - Starting health value.
52
+ * @param maxHP - Maximum health cap. If omitted, defaults to the
53
+ * initial `health` value.
54
+ */
55
+ constructor(owner: Entity, health: number, maxHP?: number) {
56
+ super(owner);
57
+ this.health = health;
58
+ this.maxHP = maxHP || health;
59
+ }
60
+
61
+ /**
62
+ * No-op — health does not change passively each frame.
63
+ *
64
+ * @param _deltaTime - Unused.
65
+ */
66
+ update(_deltaTime: number): void {}
67
+
68
+ /**
69
+ * Reduces health by the given amount, clamping at zero.
70
+ *
71
+ * @param amount - Damage to apply (positive number).
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * healthkit.takeDamage(30);
76
+ * ```
77
+ */
78
+ takeDamage(amount: number): void {
79
+ this.health = Math.max(0, this.health - amount);
80
+ }
81
+
82
+ /**
83
+ * Increases health by the given amount, clamping at
84
+ * {@link HealthKit.maxHP}.
85
+ *
86
+ * @param amount - Health to restore (positive number).
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * healthkit.heal(50);
91
+ * ```
92
+ */
93
+ heal(amount: number): void {
94
+ this.health = Math.min(this.maxHP, this.health + amount);
95
+ }
96
+
97
+ /**
98
+ * Returns the current health value.
99
+ *
100
+ * @returns Current HP.
101
+ */
102
+ getHealth(): number {
103
+ return this.health;
104
+ }
105
+
106
+ /**
107
+ * Returns the maximum health cap.
108
+ *
109
+ * @returns Maximum HP.
110
+ */
111
+ getMaxHealth(): number {
112
+ return this.maxHP;
113
+ }
114
+
115
+ /**
116
+ * Updates the maximum health cap.
117
+ *
118
+ * If the current health exceeds the new cap it is clamped down.
119
+ *
120
+ * @param value - The new maximum HP.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * healthkit.setMaxHealth(150);
125
+ * ```
126
+ */
127
+ setMaxHealth(value: number): void {
128
+ this.maxHP = value;
129
+ if (this.health > this.maxHP) {
130
+ this.health = this.maxHP;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Whether the entity is dead (health is zero or below).
136
+ *
137
+ * @returns `true` if `health <= 0`.
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * if (healthkit.isDead()) {
142
+ * entity.destroy();
143
+ * }
144
+ * ```
145
+ */
146
+ isDead(): boolean {
147
+ return this.health <= 0;
148
+ }
149
+
150
+ /**
151
+ * Returns health as a normalised ratio in the range `[0, 1]`.
152
+ *
153
+ * Useful for rendering health bars.
154
+ *
155
+ * @returns `health / maxHP`, or `0` if `maxHP` is zero.
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * const barWidth = 100 * healthkit.getHealthPercent();
160
+ * ctx.fillRect(x, y, barWidth, 8);
161
+ * ```
162
+ */
163
+ getHealthPercent(): number {
164
+ return this.maxHP > 0 ? this.health / this.maxHP : 0;
165
+ }
166
+ }