@dryanovski/gamefoo 0.2.1 → 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.
- package/dist/core/animate.js +147 -0
- package/dist/core/asset.js +74 -0
- package/dist/core/behaviour.js +88 -0
- package/dist/core/behaviours/collidable.js +186 -0
- package/dist/core/behaviours/control.js +75 -0
- package/dist/core/behaviours/healtkit.js +153 -0
- package/dist/core/behaviours/sprite_render.js +193 -0
- package/dist/core/camera.js +134 -0
- package/dist/core/engine.d.ts +1 -1
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +527 -0
- package/dist/core/fonts/font_bitmap.js +205 -0
- package/dist/core/fonts/font_bitmap_prebuild.js +137 -0
- package/dist/core/fonts/internal/font_3x5.js +169 -0
- package/dist/core/fonts/internal/font_4x6.js +171 -0
- package/dist/core/fonts/internal/font_5x5.js +129 -0
- package/dist/core/fonts/internal/font_6x8.js +171 -0
- package/dist/core/fonts/internal/font_8x13.js +171 -0
- package/dist/core/fonts/internal/font_8x8.js +171 -0
- package/dist/core/game_object_register.js +134 -0
- package/dist/core/input.js +170 -0
- package/dist/core/sprite.js +222 -0
- package/dist/core/utils/perlin_noise.js +183 -0
- package/dist/core/world.js +304 -0
- package/dist/debug/monitor.js +47 -0
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/log.js +42 -0
- package/dist/entities/dynamic_entity.js +99 -0
- package/dist/entities/entity.js +283 -0
- package/dist/entities/player.js +93 -0
- package/dist/entities/text.js +62 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -29
- package/dist/subsystems/camera_system.d.ts +2 -2
- package/dist/subsystems/camera_system.d.ts.map +1 -1
- package/dist/subsystems/camera_system.js +41 -0
- package/dist/subsystems/collision_system.js +17 -0
- package/dist/subsystems/monitor_system.js +20 -0
- package/dist/subsystems/object_system.d.ts +1 -1
- package/dist/subsystems/object_system.d.ts.map +1 -1
- package/dist/subsystems/object_system.js +29 -0
- package/dist/subsystems/types.d.ts +1 -1
- package/dist/subsystems/types.d.ts.map +1 -1
- package/dist/subsystems/types.js +0 -0
- package/dist/types.js +10 -0
- package/package.json +17 -6
- package/src/core/animate.ts +159 -0
- package/src/core/asset.ts +76 -0
- package/src/core/behaviour.ts +145 -0
- package/src/core/behaviours/collidable.ts +296 -0
- package/src/core/behaviours/control.ts +80 -0
- package/src/core/behaviours/healtkit.ts +166 -0
- package/src/core/behaviours/sprite_render.ts +216 -0
- package/src/core/camera.ts +145 -0
- package/src/core/engine.ts +607 -0
- package/src/core/fonts/font_bitmap.ts +232 -0
- package/src/core/fonts/font_bitmap_prebuild.ts +141 -0
- package/src/core/fonts/internal/font_3x5.ts +178 -0
- package/src/core/fonts/internal/font_4x6.ts +180 -0
- package/src/core/fonts/internal/font_5x5.ts +137 -0
- package/src/core/fonts/internal/font_6x8.ts +180 -0
- package/src/core/fonts/internal/font_8x13.ts +180 -0
- package/src/core/fonts/internal/font_8x8.ts +180 -0
- package/src/core/game_object_register.ts +146 -0
- package/src/core/input.ts +182 -0
- package/src/core/sprite.ts +339 -0
- package/src/core/utils/perlin_noise.ts +196 -0
- package/src/core/world.ts +331 -0
- package/src/debug/monitor.ts +60 -0
- package/src/decorators/index.ts +1 -0
- package/src/decorators/log.ts +45 -0
- package/src/entities/dynamic_entity.ts +106 -0
- package/src/entities/entity.ts +322 -0
- package/src/entities/player.ts +99 -0
- package/src/entities/text.ts +72 -0
- package/src/index.ts +51 -0
- package/src/subsystems/camera_system.ts +52 -0
- package/src/subsystems/collision_system.ts +21 -0
- package/src/subsystems/monitor_system.ts +26 -0
- package/src/subsystems/object_system.ts +37 -0
- package/src/subsystems/types.ts +46 -0
- package/src/types.ts +178 -0
- 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
|
+
}
|