@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.
- 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,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
|
+
}
|