@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,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame-based sprite animation controller.
|
|
3
|
+
*
|
|
4
|
+
* `Animate` steps through a sequence of spritesheet cells at a given
|
|
5
|
+
* frame rate. It tracks elapsed time internally and advances the
|
|
6
|
+
* current frame index each time the interval elapses.
|
|
7
|
+
*
|
|
8
|
+
* > **Note:** The {@link Animate.draw} method is currently a stub —
|
|
9
|
+
* > it resolves the correct frame but does not yet perform the actual
|
|
10
|
+
* > `drawImage` call. Use {@link SpriteRender} for production
|
|
11
|
+
* > sprite rendering.
|
|
12
|
+
*
|
|
13
|
+
* @category Core
|
|
14
|
+
* @since 0.1.0
|
|
15
|
+
*
|
|
16
|
+
* @example Creating a walk animation
|
|
17
|
+
* ```ts
|
|
18
|
+
* const walkFrames = [
|
|
19
|
+
* { col: 0, row: 0 },
|
|
20
|
+
* { col: 1, row: 0 },
|
|
21
|
+
* { col: 2, row: 0 },
|
|
22
|
+
* { col: 3, row: 0 },
|
|
23
|
+
* ];
|
|
24
|
+
*
|
|
25
|
+
* const anim = new Animate("walk", walkFrames, 32, 32, 12);
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example Updating in a game loop
|
|
29
|
+
* ```ts
|
|
30
|
+
* function update(delta: number) {
|
|
31
|
+
* anim.update(delta);
|
|
32
|
+
* anim.draw(ctx, entity.x, entity.y);
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @see {@link SpriteRender} — behaviour-based alternative for entity rendering
|
|
37
|
+
* @see {@link Sprite} — spritesheet metadata container
|
|
38
|
+
*/
|
|
39
|
+
export default class Animate {
|
|
40
|
+
/** Identifier for this animation (e.g. `"walk"`, `"idle"`). */
|
|
41
|
+
key;
|
|
42
|
+
/** Ordered list of spritesheet cells that make up the animation. */
|
|
43
|
+
frames;
|
|
44
|
+
/** Width of a single frame in pixels. */
|
|
45
|
+
frameW;
|
|
46
|
+
/** Height of a single frame in pixels. */
|
|
47
|
+
frameH;
|
|
48
|
+
/**
|
|
49
|
+
* Index into {@link Animate.frames} of the frame currently being
|
|
50
|
+
* displayed.
|
|
51
|
+
*
|
|
52
|
+
* @defaultValue `0`
|
|
53
|
+
*/
|
|
54
|
+
currentFrame = 0;
|
|
55
|
+
/**
|
|
56
|
+
* Milliseconds accumulated since the last frame advance.
|
|
57
|
+
*
|
|
58
|
+
* @defaultValue `0`
|
|
59
|
+
*/
|
|
60
|
+
elapsed = 0;
|
|
61
|
+
/** Computed time between frames in milliseconds (`1000 / fps`). */
|
|
62
|
+
interval;
|
|
63
|
+
/** Playback speed in frames per second. */
|
|
64
|
+
fps;
|
|
65
|
+
/**
|
|
66
|
+
* Creates a new animation sequence.
|
|
67
|
+
*
|
|
68
|
+
* @param key - A unique name for this animation (used as a look-up key).
|
|
69
|
+
* @param frames - An ordered array of `{ col, row }` cells from the
|
|
70
|
+
* spritesheet.
|
|
71
|
+
* @param frameW - Width of each frame in pixels.
|
|
72
|
+
* @param frameH - Height of each frame in pixels.
|
|
73
|
+
* @param fps - Playback speed in frames per second.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const idle = new Animate(
|
|
78
|
+
* "idle",
|
|
79
|
+
* [{ col: 0, row: 1 }, { col: 1, row: 1 }],
|
|
80
|
+
* 64, 64,
|
|
81
|
+
* 8,
|
|
82
|
+
* );
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
constructor(key, frames, frameW, frameH, fps) {
|
|
86
|
+
this.key = key;
|
|
87
|
+
this.frames = frames;
|
|
88
|
+
this.frameW = frameW;
|
|
89
|
+
this.frameH = frameH;
|
|
90
|
+
this.fps = fps;
|
|
91
|
+
this.interval = 1000 / this.fps;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Advances the animation clock and moves to the next frame when the
|
|
95
|
+
* interval has elapsed.
|
|
96
|
+
*
|
|
97
|
+
* @param delta - Time elapsed since the last call, **in milliseconds**.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* // Inside a requestAnimationFrame loop:
|
|
102
|
+
* anim.update(deltaMs);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
update(delta) {
|
|
106
|
+
this.elapsed += delta;
|
|
107
|
+
if (this.elapsed >= this.interval) {
|
|
108
|
+
this.currentFrame = (this.currentFrame + 1) % this.frames.length;
|
|
109
|
+
this.elapsed = 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Draws the current animation frame to the canvas.
|
|
114
|
+
*
|
|
115
|
+
* @remarks
|
|
116
|
+
* This method is currently a **stub**. It resolves the correct
|
|
117
|
+
* `{ col, row }` cell but does not perform the actual
|
|
118
|
+
* `ctx.drawImage()` call. Wire it to {@link Asset} or use
|
|
119
|
+
* {@link SpriteRender} for full rendering.
|
|
120
|
+
*
|
|
121
|
+
* @param _ctx - The canvas 2-D rendering context.
|
|
122
|
+
* @param _destX - Destination X coordinate on the canvas.
|
|
123
|
+
* @param _destY - Destination Y coordinate on the canvas.
|
|
124
|
+
*/
|
|
125
|
+
draw(_ctx, _destX, _destY) {
|
|
126
|
+
const frame = this.frames[this.currentFrame];
|
|
127
|
+
if (!frame)
|
|
128
|
+
return;
|
|
129
|
+
const { col: _col, row: _row } = frame;
|
|
130
|
+
// Asset.drawFrame(ctx, key, col, row, frameW, frameH, destX, destY)
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Resets the animation to its first frame.
|
|
134
|
+
*
|
|
135
|
+
* Call this when switching animations or restarting a sequence.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* anim.reset();
|
|
140
|
+
* anim.update(0); // ensures frame 0 is active
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
reset() {
|
|
144
|
+
this.currentFrame = 0;
|
|
145
|
+
this.elapsed = 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static image asset loader with an in-memory cache.
|
|
3
|
+
*
|
|
4
|
+
* `Asset` wraps the native `Image` constructor with a `Promise`-based
|
|
5
|
+
* API and caches loaded images by URL so repeated requests for the same
|
|
6
|
+
* source resolve instantly.
|
|
7
|
+
*
|
|
8
|
+
* @category Core
|
|
9
|
+
* @since 0.1.0
|
|
10
|
+
*
|
|
11
|
+
* @example Loading an image
|
|
12
|
+
* ```ts
|
|
13
|
+
* const image = await Asset.load("sprites/hero.png");
|
|
14
|
+
* ctx.drawImage(image, 0, 0);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @example Pre-loading multiple assets
|
|
18
|
+
* ```ts
|
|
19
|
+
* await Promise.all([
|
|
20
|
+
* Asset.load("sprites/hero.png"),
|
|
21
|
+
* Asset.load("sprites/enemy.png"),
|
|
22
|
+
* Asset.load("tiles/grass.png"),
|
|
23
|
+
* ]);
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @see {@link Sprite} — consumes loaded images for spritesheet slicing
|
|
27
|
+
*/
|
|
28
|
+
export default class Asset {
|
|
29
|
+
/**
|
|
30
|
+
* Internal cache mapping source URLs to their loaded
|
|
31
|
+
* `HTMLImageElement` instances.
|
|
32
|
+
*/
|
|
33
|
+
static cache = new Map();
|
|
34
|
+
/**
|
|
35
|
+
* Loads an image from the given URL.
|
|
36
|
+
*
|
|
37
|
+
* If the image has been loaded before, the cached `HTMLImageElement`
|
|
38
|
+
* is returned immediately (the `Promise` resolves synchronously on
|
|
39
|
+
* the microtask queue).
|
|
40
|
+
*
|
|
41
|
+
* @param src - URL or relative path of the image to load.
|
|
42
|
+
* @returns A `Promise` that resolves with the loaded
|
|
43
|
+
* `HTMLImageElement`.
|
|
44
|
+
*
|
|
45
|
+
* @throws {Error} If the image fails to load (e.g. 404 or network
|
|
46
|
+
* error). The error message includes the failing `src`.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* try {
|
|
51
|
+
* const img = await Asset.load("missing.png");
|
|
52
|
+
* } catch (err) {
|
|
53
|
+
* console.error(err); // "Failed to load image: missing.png"
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
static async load(src) {
|
|
58
|
+
const cache = Asset.cache.get(src);
|
|
59
|
+
if (cache) {
|
|
60
|
+
return cache;
|
|
61
|
+
}
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const image = new Image();
|
|
64
|
+
image.onload = () => {
|
|
65
|
+
Asset.cache.set(src, image);
|
|
66
|
+
resolve(image);
|
|
67
|
+
};
|
|
68
|
+
image.onerror = (_error) => {
|
|
69
|
+
reject(new Error(`Failed to load image: ${src}`));
|
|
70
|
+
};
|
|
71
|
+
image.src = src;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class for all entity behaviours in the GameFoo engine.
|
|
3
|
+
*
|
|
4
|
+
* A **behaviour** is a self-contained unit of logic (input handling,
|
|
5
|
+
* collision response, health tracking, rendering, etc.) that can be
|
|
6
|
+
* attached to any {@link Entity} at runtime via
|
|
7
|
+
* {@link Entity.attachBehaviour}.
|
|
8
|
+
*
|
|
9
|
+
* Subclasses **must** implement:
|
|
10
|
+
* - {@link Behaviour.type | type} — a unique string identifier (e.g. `"control"`, `"healthkit"`).
|
|
11
|
+
* - {@link Behaviour.update | update} — called once per frame with `deltaTime`.
|
|
12
|
+
*
|
|
13
|
+
* Subclasses **may** override:
|
|
14
|
+
* - {@link Behaviour.render | render} — draw debug visuals or overlays.
|
|
15
|
+
* - {@link Behaviour.onAttach | onAttach} — setup hook when added to an entity.
|
|
16
|
+
* - {@link Behaviour.onDetach | onDetach} — teardown hook when removed.
|
|
17
|
+
*
|
|
18
|
+
* @typeParam T - The entity type this behaviour operates on.
|
|
19
|
+
* Defaults to {@link Entity}; narrow it to {@link DynamicEntity} or
|
|
20
|
+
* {@link Player} when the behaviour needs velocity, speed, etc.
|
|
21
|
+
*
|
|
22
|
+
* @category Behaviours
|
|
23
|
+
* @since 0.1.0
|
|
24
|
+
*
|
|
25
|
+
* @example Creating a custom behaviour
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { Behaviour, type Entity } from "gamefoo";
|
|
28
|
+
*
|
|
29
|
+
* class Gravity extends Behaviour<Entity> {
|
|
30
|
+
* readonly type = "gravity";
|
|
31
|
+
*
|
|
32
|
+
* update(deltaTime: number): void {
|
|
33
|
+
* this.owner.y += 9.8 * 60 * deltaTime;
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @example Attaching to an entity
|
|
39
|
+
* ```ts
|
|
40
|
+
* const entity = new Player("hero", 100, 100, 32, 32);
|
|
41
|
+
* entity.attachBehaviour(new Gravity(entity));
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @see {@link Entity.attachBehaviour}
|
|
45
|
+
* @see {@link Entity.detachBehaviour}
|
|
46
|
+
*/
|
|
47
|
+
export class Behaviour {
|
|
48
|
+
/**
|
|
49
|
+
* Reference to the entity that owns this behaviour.
|
|
50
|
+
* Available to subclasses for reading and mutating entity state.
|
|
51
|
+
*/
|
|
52
|
+
owner;
|
|
53
|
+
/**
|
|
54
|
+
* Execution priority — lower numbers run first.
|
|
55
|
+
*
|
|
56
|
+
* When an entity has multiple behaviours, they are sorted by priority
|
|
57
|
+
* before each update/render pass.
|
|
58
|
+
*
|
|
59
|
+
* @defaultValue `1`
|
|
60
|
+
*/
|
|
61
|
+
priority = 1;
|
|
62
|
+
/**
|
|
63
|
+
* Whether this behaviour is currently active.
|
|
64
|
+
*
|
|
65
|
+
* Disabled behaviours are skipped during both
|
|
66
|
+
* {@link Entity.updateBehaviours} and {@link Entity.renderBehaviours}.
|
|
67
|
+
*
|
|
68
|
+
* @defaultValue `true`
|
|
69
|
+
*/
|
|
70
|
+
enabled = true;
|
|
71
|
+
/**
|
|
72
|
+
* Derived look-up key, equal to {@link Behaviour.type} in lowercase.
|
|
73
|
+
*
|
|
74
|
+
* Used internally by the entity's behaviour map so that look-ups are
|
|
75
|
+
* case-insensitive.
|
|
76
|
+
*/
|
|
77
|
+
get key() {
|
|
78
|
+
return this.type.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Creates a new behaviour bound to the given entity.
|
|
82
|
+
*
|
|
83
|
+
* @param owner - The entity this behaviour will operate on.
|
|
84
|
+
*/
|
|
85
|
+
constructor(owner) {
|
|
86
|
+
this.owner = owner;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Behaviour } from "../behaviour";
|
|
2
|
+
/**
|
|
3
|
+
* Collision behaviour that can be attached to any {@link Entity}.
|
|
4
|
+
*
|
|
5
|
+
* When attached, the `Collidable` automatically registers itself with
|
|
6
|
+
* the engine's {@link World} (via {@link Collidable.onAttach}) and
|
|
7
|
+
* unregisters on detach. Each frame the `World` queries the collider's
|
|
8
|
+
* shape, bounds, and tags to determine intersections.
|
|
9
|
+
*
|
|
10
|
+
* @category Behaviours
|
|
11
|
+
* @since 0.1.0
|
|
12
|
+
*
|
|
13
|
+
* @example Creating and attaching a box collider
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { Collidable, Entity, type CollisionInfo } from "gamefoo";
|
|
16
|
+
*
|
|
17
|
+
* const entity = new Enemy("goblin", 100, 200, 30, 30);
|
|
18
|
+
*
|
|
19
|
+
* entity.attachBehaviour(
|
|
20
|
+
* new Collidable(entity, engine.collisions, {
|
|
21
|
+
* shape: { type: "aabb", width: 30, height: 30 },
|
|
22
|
+
* layer: 0,
|
|
23
|
+
* tags: new Set(["enemy"]),
|
|
24
|
+
* solid: true,
|
|
25
|
+
* collidesWith: new Set(["player"]),
|
|
26
|
+
* onCollision: (info: CollisionInfo) => {
|
|
27
|
+
* console.log(`${info.self.id} hit ${info.other.id}`);
|
|
28
|
+
* },
|
|
29
|
+
* }),
|
|
30
|
+
* );
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example Circle collider for a projectile
|
|
34
|
+
* ```ts
|
|
35
|
+
* entity.attachBehaviour(
|
|
36
|
+
* new Collidable(bullet, engine.collisions, {
|
|
37
|
+
* shape: { type: "circle", radius: 4 },
|
|
38
|
+
* tags: new Set(["bullet"]),
|
|
39
|
+
* collidesWith: new Set(["enemy"]),
|
|
40
|
+
* }),
|
|
41
|
+
* );
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @see {@link World} — the collision detection system
|
|
45
|
+
* @see {@link ColliderShape} — supported shape types
|
|
46
|
+
* @see {@link CollisionInfo} — payload delivered to callbacks
|
|
47
|
+
* @see {@link Behaviour} — abstract base class
|
|
48
|
+
*/
|
|
49
|
+
export class Collidable extends Behaviour {
|
|
50
|
+
/** @inheritDoc */
|
|
51
|
+
type = "collidable";
|
|
52
|
+
/**
|
|
53
|
+
* Geometric shape used for intersection tests.
|
|
54
|
+
*
|
|
55
|
+
* @see {@link ColliderShape}
|
|
56
|
+
*/
|
|
57
|
+
shape;
|
|
58
|
+
/**
|
|
59
|
+
* Collision layer. Only colliders sharing the same layer value are
|
|
60
|
+
* tested.
|
|
61
|
+
*
|
|
62
|
+
* @defaultValue `0`
|
|
63
|
+
*/
|
|
64
|
+
layer = 0;
|
|
65
|
+
/**
|
|
66
|
+
* Tags identifying this collider (e.g. `"player"`, `"enemy"`).
|
|
67
|
+
*
|
|
68
|
+
* @defaultValue empty `Set`
|
|
69
|
+
*/
|
|
70
|
+
tags = new Set();
|
|
71
|
+
/**
|
|
72
|
+
* Tags this collider wants to be notified about.
|
|
73
|
+
*
|
|
74
|
+
* @defaultValue empty `Set`
|
|
75
|
+
*/
|
|
76
|
+
collidesWith = new Set();
|
|
77
|
+
/**
|
|
78
|
+
* Whether this collider participates in overlap resolution.
|
|
79
|
+
*
|
|
80
|
+
* @defaultValue `false`
|
|
81
|
+
*/
|
|
82
|
+
solid = false;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the owning entity is immovable during overlap resolution.
|
|
85
|
+
*
|
|
86
|
+
* @defaultValue `false`
|
|
87
|
+
*/
|
|
88
|
+
fixed = false;
|
|
89
|
+
/**
|
|
90
|
+
* User-supplied callback invoked when a tag-matched collision is
|
|
91
|
+
* detected.
|
|
92
|
+
*/
|
|
93
|
+
onCollision;
|
|
94
|
+
/** Reference to the {@link World} this collider is registered with. */
|
|
95
|
+
world;
|
|
96
|
+
/**
|
|
97
|
+
* Creates a new collidable behaviour.
|
|
98
|
+
*
|
|
99
|
+
* @param owner - The game object entity that owns this collider.
|
|
100
|
+
* @param world - The collision {@link World} to register with.
|
|
101
|
+
* @param options - Configuration for shape, tags, solidity, and
|
|
102
|
+
* callbacks. See {@link CollidableOptions}.
|
|
103
|
+
*/
|
|
104
|
+
constructor(owner, world, options) {
|
|
105
|
+
super(owner);
|
|
106
|
+
this.world = world;
|
|
107
|
+
const size = owner.getSize();
|
|
108
|
+
this.shape = options.shape ?? {
|
|
109
|
+
type: "aabb",
|
|
110
|
+
width: size.width,
|
|
111
|
+
height: size.height,
|
|
112
|
+
};
|
|
113
|
+
this.layer = options.layer ?? 0;
|
|
114
|
+
this.tags = options.tags ?? new Set();
|
|
115
|
+
this.solid = options.solid ?? false;
|
|
116
|
+
this.fixed = options.fixed ?? false;
|
|
117
|
+
this.collidesWith = options.collidesWith ?? new Set();
|
|
118
|
+
this.onCollision = options.onCollision || (() => { });
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* No-op — collision logic lives in {@link World.detect}.
|
|
122
|
+
*
|
|
123
|
+
* @param _deltaTime - Unused.
|
|
124
|
+
*/
|
|
125
|
+
update(_deltaTime) { }
|
|
126
|
+
/**
|
|
127
|
+
* Lifecycle hook: registers this collider with the {@link World}
|
|
128
|
+
* when the behaviour is attached to an entity.
|
|
129
|
+
*
|
|
130
|
+
* @see {@link Behaviour.onAttach}
|
|
131
|
+
*/
|
|
132
|
+
onAttach() {
|
|
133
|
+
this.world.register(this);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Lifecycle hook: removes this collider from the {@link World}
|
|
137
|
+
* when the behaviour is detached.
|
|
138
|
+
*
|
|
139
|
+
* @see {@link Behaviour.onDetach}
|
|
140
|
+
*/
|
|
141
|
+
onDetach() {
|
|
142
|
+
this.world.unregister(this);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Returns the {@link Entity} that owns this behaviour.
|
|
146
|
+
*
|
|
147
|
+
* Used by the {@link World} to read and mutate entity position
|
|
148
|
+
* during overlap resolution.
|
|
149
|
+
*
|
|
150
|
+
* @returns The owning entity.
|
|
151
|
+
*/
|
|
152
|
+
getOwner() {
|
|
153
|
+
return this.owner;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Computes this collider's axis-aligned bounding rectangle in
|
|
157
|
+
* world-space, accounting for the shape's optional offset.
|
|
158
|
+
*
|
|
159
|
+
* @returns A {@link WorldBounds} rectangle.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* const bounds = collidable.getWorldBounds();
|
|
164
|
+
* // { x: 100, y: 200, width: 30, height: 30 }
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
getWorldBounds() {
|
|
168
|
+
const pos = this.owner.getPosition();
|
|
169
|
+
const offset = "offset" in this.shape && this.shape.offset ? this.shape.offset : { x: 0, y: 0 };
|
|
170
|
+
if (this.shape.type === "aabb") {
|
|
171
|
+
return {
|
|
172
|
+
x: pos.x + offset.x,
|
|
173
|
+
y: pos.y + offset.y,
|
|
174
|
+
width: this.shape.width,
|
|
175
|
+
height: this.shape.height,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const r = this.shape.radius;
|
|
179
|
+
return {
|
|
180
|
+
x: pos.x + offset.x - r,
|
|
181
|
+
y: pos.y + offset.y - r,
|
|
182
|
+
width: r * 2,
|
|
183
|
+
height: r * 2,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Behaviour } from "../behaviour";
|
|
2
|
+
/**
|
|
3
|
+
* Keyboard-driven movement behaviour for a {@link Entity}.
|
|
4
|
+
*
|
|
5
|
+
* `Control` reads the current keyboard state from an {@link Input}
|
|
6
|
+
* instance every frame and translates WASD / arrow-key presses into
|
|
7
|
+
* entity position changes. Diagonal movement is normalised so the
|
|
8
|
+
* entity moves at a consistent speed in all directions.
|
|
9
|
+
*
|
|
10
|
+
* @category Behaviours
|
|
11
|
+
* @since 0.1.0
|
|
12
|
+
*
|
|
13
|
+
* @example Attaching to a player
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { Control, Input, Player } from "gamefoo";
|
|
16
|
+
*
|
|
17
|
+
* const input = new Input();
|
|
18
|
+
* const player = new Player("hero", 400, 300, 50, 50);
|
|
19
|
+
*
|
|
20
|
+
* player.attachBehaviour(new Control(player, input));
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @see {@link Input} — the polling input manager consumed by this behaviour
|
|
24
|
+
* @see {@link Behaviour} — abstract base class
|
|
25
|
+
*/
|
|
26
|
+
export class Control extends Behaviour {
|
|
27
|
+
/** @inheritDoc */
|
|
28
|
+
type = "control";
|
|
29
|
+
/** The input manager to poll each frame. */
|
|
30
|
+
input;
|
|
31
|
+
/**
|
|
32
|
+
* Movement speed in pixels per second.
|
|
33
|
+
*
|
|
34
|
+
* @defaultValue `500`
|
|
35
|
+
*/
|
|
36
|
+
speed = 500;
|
|
37
|
+
/**
|
|
38
|
+
* Creates a new keyboard control behaviour.
|
|
39
|
+
*
|
|
40
|
+
* @param owner - The game object entity whose position will be updated.
|
|
41
|
+
* @param input - The {@link Input} instance to read key state from.
|
|
42
|
+
*/
|
|
43
|
+
constructor(owner, input) {
|
|
44
|
+
super(owner);
|
|
45
|
+
this.input = input;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reads the current key state and moves the owner entity.
|
|
49
|
+
*
|
|
50
|
+
* Supported keys: `W` / `ArrowUp`, `S` / `ArrowDown`,
|
|
51
|
+
* `A` / `ArrowLeft`, `D` / `ArrowRight`.
|
|
52
|
+
*
|
|
53
|
+
* Diagonal input is normalised so the effective speed remains
|
|
54
|
+
* constant regardless of direction.
|
|
55
|
+
*
|
|
56
|
+
* @param deltaTime - Seconds elapsed since the previous frame.
|
|
57
|
+
*/
|
|
58
|
+
update(deltaTime) {
|
|
59
|
+
let dx = 0;
|
|
60
|
+
let dy = 0;
|
|
61
|
+
if (this.input.isKeyDown("a") || this.input.isKeyDown("arrowleft"))
|
|
62
|
+
dx -= 1;
|
|
63
|
+
if (this.input.isKeyDown("d") || this.input.isKeyDown("arrowright"))
|
|
64
|
+
dx += 1;
|
|
65
|
+
if (this.input.isKeyDown("w") || this.input.isKeyDown("arrowup"))
|
|
66
|
+
dy -= 1;
|
|
67
|
+
if (this.input.isKeyDown("s") || this.input.isKeyDown("arrowdown"))
|
|
68
|
+
dy += 1;
|
|
69
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
70
|
+
if (len > 0) {
|
|
71
|
+
this.owner.x += (dx / len) * this.speed * deltaTime;
|
|
72
|
+
this.owner.y += (dy / len) * this.speed * deltaTime;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|