@caperjs/plugin-crunch 0.1.0
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/LICENSE +22 -0
- package/README.md +205 -0
- package/lib/Actor.d.ts +227 -0
- package/lib/Actor.d.ts.map +1 -0
- package/lib/CrunchPhysicsPlugin.d.ts +508 -0
- package/lib/CrunchPhysicsPlugin.d.ts.map +1 -0
- package/lib/Entity.d.ts +377 -0
- package/lib/Entity.d.ts.map +1 -0
- package/lib/Group.d.ts +122 -0
- package/lib/Group.d.ts.map +1 -0
- package/lib/Sensor.d.ts +161 -0
- package/lib/Sensor.d.ts.map +1 -0
- package/lib/Solid.d.ts +109 -0
- package/lib/Solid.d.ts.map +1 -0
- package/lib/System.d.ts +265 -0
- package/lib/System.d.ts.map +1 -0
- package/lib/caper-plugin-crunch.mjs +1331 -0
- package/lib/caper-plugin-crunch.mjs.map +1 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/interfaces.d.ts +32 -0
- package/lib/interfaces.d.ts.map +1 -0
- package/lib/types.d.ts +246 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/utils.d.ts +63 -0
- package/lib/utils.d.ts.map +1 -0
- package/lib/version.d.ts +2 -0
- package/lib/version.d.ts.map +1 -0
- package/package.json +42 -0
- package/src/Actor.ts +726 -0
- package/src/CrunchPhysicsPlugin.ts +718 -0
- package/src/Entity.ts +762 -0
- package/src/Group.ts +181 -0
- package/src/Sensor.ts +427 -0
- package/src/Solid.ts +380 -0
- package/src/System.ts +1546 -0
- package/src/collision-layers.d.ts +87 -0
- package/src/index.ts +18 -0
- package/src/interfaces.ts +33 -0
- package/src/types.ts +336 -0
- package/src/utils.ts +72 -0
- package/src/version.ts +1 -0
package/src/Actor.ts
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import { Entity } from './Entity';
|
|
2
|
+
import { Solid } from './Solid';
|
|
3
|
+
import {
|
|
4
|
+
ActorCollisionResult,
|
|
5
|
+
CollisionResult,
|
|
6
|
+
EntityData,
|
|
7
|
+
PhysicsEntityConfig,
|
|
8
|
+
PhysicsEntityType,
|
|
9
|
+
Vector2,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Dynamic physics entity that can move and collide with other entities.
|
|
14
|
+
* Actors are typically used for players, enemies, projectiles, and other moving game objects.
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Velocity-based movement with gravity
|
|
18
|
+
* - Collision detection and response
|
|
19
|
+
* - Solid surface detection (riding)
|
|
20
|
+
* - Automatic culling when out of bounds
|
|
21
|
+
* - Actor-to-actor collision detection
|
|
22
|
+
*
|
|
23
|
+
* @typeParam T - Application type, defaults to base Application
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // Create a player actor
|
|
28
|
+
* class Player extends Actor {
|
|
29
|
+
* constructor() {
|
|
30
|
+
* super({
|
|
31
|
+
* type: 'Player',
|
|
32
|
+
* position: [100, 100],
|
|
33
|
+
* size: [32, 64],
|
|
34
|
+
* view: playerSprite
|
|
35
|
+
* });
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* // Handle collisions
|
|
39
|
+
* onCollide(result: CollisionResult) {
|
|
40
|
+
* if (result.solid.type === 'Spike') {
|
|
41
|
+
* this.die();
|
|
42
|
+
* }
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* // Handle actor-to-actor collisions
|
|
46
|
+
* onActorCollide(result: ActorCollisionResult) {
|
|
47
|
+
* if (result.actor.type === 'Enemy') {
|
|
48
|
+
* this.takeDamage(10);
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
*
|
|
52
|
+
* // Custom movement
|
|
53
|
+
* update(dt: number) {
|
|
54
|
+
* super.update(dt);
|
|
55
|
+
*
|
|
56
|
+
* // Move left/right
|
|
57
|
+
* if (this.app.input.isKeyDown('ArrowLeft')) {
|
|
58
|
+
* this.velocity.x = -200;
|
|
59
|
+
* } else if (this.app.input.isKeyDown('ArrowRight')) {
|
|
60
|
+
* this.velocity.x = 200;
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* // Jump when on ground
|
|
64
|
+
* if (this.app.input.isKeyPressed('Space') && this.isRidingSolid()) {
|
|
65
|
+
* this.velocity.y = -400;
|
|
66
|
+
* }
|
|
67
|
+
* }
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export class Actor<D extends EntityData = EntityData> extends Entity<D> {
|
|
72
|
+
public readonly entityType: PhysicsEntityType = 'Actor';
|
|
73
|
+
|
|
74
|
+
/** Current velocity in pixels per second */
|
|
75
|
+
public velocity: Vector2 = { x: 0, y: 0 };
|
|
76
|
+
|
|
77
|
+
/** Whether actor-to-actor collisions are disabled for this actor */
|
|
78
|
+
public disableActorCollisions: boolean = false;
|
|
79
|
+
|
|
80
|
+
/** Whether the actor should be removed when culled (out of bounds) */
|
|
81
|
+
public shouldRemoveOnCull: boolean = true;
|
|
82
|
+
|
|
83
|
+
/** List of current frame collisions */
|
|
84
|
+
public collisions: CollisionResult[] = [];
|
|
85
|
+
|
|
86
|
+
/** List of current frame actor-to-actor collisions */
|
|
87
|
+
public actorCollisions: ActorCollisionResult[] = [];
|
|
88
|
+
|
|
89
|
+
/** Cache for isRidingSolid check */
|
|
90
|
+
private _isRidingSolidCache: boolean | null = null;
|
|
91
|
+
|
|
92
|
+
/** Tracks which solid is currently carrying this actor in the current frame */
|
|
93
|
+
private _carriedBy: Solid | null = null;
|
|
94
|
+
private _carriedByOverlap: number = 0;
|
|
95
|
+
|
|
96
|
+
/** Tracks the grid cells this actor currently occupies */
|
|
97
|
+
private _currentGridCells: string[] = [];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Initialize or reinitialize the actor with new configuration.
|
|
101
|
+
*
|
|
102
|
+
* @param config - Configuration for the actor
|
|
103
|
+
*/
|
|
104
|
+
public init(config: PhysicsEntityConfig<D>): void {
|
|
105
|
+
super.init(config);
|
|
106
|
+
// Reset velocity and carried state
|
|
107
|
+
this.velocity = { x: 0, y: 0 };
|
|
108
|
+
this._isRidingSolidCache = null;
|
|
109
|
+
this._carriedBy = null;
|
|
110
|
+
this._carriedByOverlap = 0;
|
|
111
|
+
this.actorCollisions = [];
|
|
112
|
+
this._currentGridCells = [];
|
|
113
|
+
|
|
114
|
+
if (config.disableActorCollisions !== undefined) {
|
|
115
|
+
this.disableActorCollisions = config.disableActorCollisions;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add actor to grid initially if actor collisions are enabled
|
|
119
|
+
if (this.system.enableActorCollisions && !this.disableActorCollisions) {
|
|
120
|
+
this.updateGridCells();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Called at the start of each update to prepare for collision checks.
|
|
126
|
+
*/
|
|
127
|
+
public preUpdate(): void {
|
|
128
|
+
if (!this.active) return;
|
|
129
|
+
|
|
130
|
+
this.collisions = [];
|
|
131
|
+
this.actorCollisions = [];
|
|
132
|
+
// Reset the cache at the start of each update
|
|
133
|
+
this._isRidingSolidCache = null;
|
|
134
|
+
this._carriedBy = null;
|
|
135
|
+
this._carriedByOverlap = 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Updates the actor's position based on velocity and handles collisions.
|
|
140
|
+
*
|
|
141
|
+
* @param dt - Delta time in seconds
|
|
142
|
+
*/
|
|
143
|
+
public update(dt: number): void {
|
|
144
|
+
if (!this.active) return;
|
|
145
|
+
|
|
146
|
+
// Ensure velocity is valid
|
|
147
|
+
if (!this.isRidingSolid()) {
|
|
148
|
+
this.velocity.y += this.system.gravity * dt;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Clamp velocity
|
|
152
|
+
this.velocity.x = Math.min(Math.max(this.velocity.x, -this.system.maxVelocity), this.system.maxVelocity);
|
|
153
|
+
this.velocity.y = Math.min(Math.max(this.velocity.y, -this.system.maxVelocity), this.system.maxVelocity);
|
|
154
|
+
|
|
155
|
+
// Move horizontally
|
|
156
|
+
if (this.velocity.x !== 0) {
|
|
157
|
+
this.moveX(this.velocity.x * dt);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Move vertically
|
|
161
|
+
if (this.velocity.y !== 0) {
|
|
162
|
+
this.moveY(this.velocity.y * dt);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.system.enableActorCollisions) {
|
|
166
|
+
this.updateGridCells();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Update view
|
|
170
|
+
this.updateView();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Called after update to handle post-movement effects.
|
|
175
|
+
*/
|
|
176
|
+
public postUpdate(): void {
|
|
177
|
+
if (!this.active) return;
|
|
178
|
+
|
|
179
|
+
if (this.isRidingSolid()) {
|
|
180
|
+
this.velocity.y = 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Resets the actor to its initial state.
|
|
186
|
+
*/
|
|
187
|
+
public reset(): void {
|
|
188
|
+
super.reset();
|
|
189
|
+
|
|
190
|
+
this._isRidingSolidCache = null;
|
|
191
|
+
this._carriedBy = null;
|
|
192
|
+
this._carriedByOverlap = 0;
|
|
193
|
+
this.velocity = { x: 0, y: 0 };
|
|
194
|
+
|
|
195
|
+
this.updatePosition();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Called when the actor is culled (goes out of bounds).
|
|
200
|
+
* Override this to handle culling differently.
|
|
201
|
+
*/
|
|
202
|
+
public onCull(): void {
|
|
203
|
+
// Default behavior: destroy the view
|
|
204
|
+
this.view?.destroy();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Called when this actor collides with a solid.
|
|
209
|
+
* Override this method to implement custom collision response.
|
|
210
|
+
*
|
|
211
|
+
* @param result - Information about the collision
|
|
212
|
+
*/
|
|
213
|
+
public onCollide(result: CollisionResult): void {
|
|
214
|
+
// Default implementation does nothing
|
|
215
|
+
// Override this in your actor subclass to handle collisions
|
|
216
|
+
void result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Called when this actor collides with another actor.
|
|
221
|
+
* Override this method to implement custom actor-to-actor collision response.
|
|
222
|
+
*
|
|
223
|
+
* @param result - Information about the actor collision
|
|
224
|
+
*/
|
|
225
|
+
public onActorCollide(result: ActorCollisionResult): void {
|
|
226
|
+
// Default implementation does nothing
|
|
227
|
+
// Override this in your actor subclass to handle actor-to-actor collisions
|
|
228
|
+
void result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Checks if this actor is riding the given solid.
|
|
233
|
+
* An actor is riding if it's directly above the solid.
|
|
234
|
+
*
|
|
235
|
+
* @param solid - The solid to check against
|
|
236
|
+
* @returns True if riding the solid
|
|
237
|
+
*/
|
|
238
|
+
public isRiding(solid: Solid): boolean {
|
|
239
|
+
// Skip if solid has no collisions
|
|
240
|
+
if (!solid.collideable) return false;
|
|
241
|
+
|
|
242
|
+
// Check collision layers and masks
|
|
243
|
+
// An actor can only ride a solid if their collision layers/masks allow interaction
|
|
244
|
+
if ((this.collisionLayer & solid.collisionMask) === 0 || (solid.collisionLayer & this.collisionMask) === 0) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// If we're already being carried by a different solid this frame,
|
|
249
|
+
// we can't be riding this one
|
|
250
|
+
if (this._carriedBy && this._carriedBy !== solid) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Must be directly above the solid (within 1 pixel)
|
|
255
|
+
const actorBottom = this.y + this.height;
|
|
256
|
+
const onTop = Math.abs(actorBottom - solid.y) <= 1;
|
|
257
|
+
|
|
258
|
+
// Must be horizontally overlapping
|
|
259
|
+
const overlap = this.x + this.width > solid.x && this.x < solid.x + solid.width;
|
|
260
|
+
const overlapWidth = Math.min(this.x + this.width, solid.x + solid.width) - Math.max(this.x, solid.x);
|
|
261
|
+
|
|
262
|
+
const isRiding = onTop && overlap;
|
|
263
|
+
|
|
264
|
+
if (isRiding && overlapWidth > this._carriedByOverlap) {
|
|
265
|
+
this._carriedBy = solid;
|
|
266
|
+
this._carriedByOverlap = overlapWidth;
|
|
267
|
+
}
|
|
268
|
+
return isRiding;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Checks if this actor is riding any solid in the physics system.
|
|
273
|
+
* Uses caching to optimize multiple checks per frame.
|
|
274
|
+
*
|
|
275
|
+
* @returns True if riding any solid
|
|
276
|
+
*/
|
|
277
|
+
public isRidingSolid(): boolean {
|
|
278
|
+
// Return cached value if available
|
|
279
|
+
if (this._isRidingSolidCache !== null) {
|
|
280
|
+
return this._isRidingSolidCache;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Calculate and cache the result
|
|
284
|
+
const solids = this.getSolidsAt(this.x, this.y + 1);
|
|
285
|
+
this._isRidingSolidCache = solids.some((solid) => this.isRiding(solid));
|
|
286
|
+
return this._isRidingSolidCache;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Called when the actor is squeezed between solids.
|
|
291
|
+
* Override this to handle squishing differently.
|
|
292
|
+
*/
|
|
293
|
+
public squish(result: CollisionResult): void {
|
|
294
|
+
void result;
|
|
295
|
+
// do something
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Updates the actor's grid cells in the spatial partitioning system.
|
|
300
|
+
* This is called when the actor moves or when its size changes.
|
|
301
|
+
*/
|
|
302
|
+
public updateGridCells(): void {
|
|
303
|
+
// Skip if actor collisions are disabled system-wide or for this actor specifically
|
|
304
|
+
if (this.disableActorCollisions) return;
|
|
305
|
+
|
|
306
|
+
this.system.updateActorInGrid(this);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Gets the current grid cells this actor occupies
|
|
311
|
+
*/
|
|
312
|
+
public get currentGridCells(): string[] {
|
|
313
|
+
return this._currentGridCells;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Sets the current grid cells this actor occupies
|
|
318
|
+
*/
|
|
319
|
+
public set currentGridCells(cells: string[]) {
|
|
320
|
+
this._currentGridCells = cells;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Moves the actor horizontally, checking for collisions with solids.
|
|
325
|
+
*
|
|
326
|
+
* @param amount - Distance to move in pixels
|
|
327
|
+
* @param collisionHandler - Optional callback for handling collisions
|
|
328
|
+
* @returns Array of collision results
|
|
329
|
+
*/
|
|
330
|
+
public moveX(
|
|
331
|
+
amount: number,
|
|
332
|
+
collisionHandler?: (result: CollisionResult) => void,
|
|
333
|
+
pushingSolid?: Solid,
|
|
334
|
+
): CollisionResult[] {
|
|
335
|
+
// Early return if inactive or zero movement
|
|
336
|
+
if (!this.active || amount === 0) return [];
|
|
337
|
+
|
|
338
|
+
this._xRemainder += amount;
|
|
339
|
+
const move = Math.round(this._xRemainder);
|
|
340
|
+
|
|
341
|
+
// Early return if rounded movement is zero
|
|
342
|
+
if (move === 0) return [];
|
|
343
|
+
|
|
344
|
+
const collisions: CollisionResult[] = [];
|
|
345
|
+
|
|
346
|
+
// Cache collision layer and mask for faster access
|
|
347
|
+
const actorLayer = this.collisionLayer;
|
|
348
|
+
const actorMask = this.collisionMask;
|
|
349
|
+
|
|
350
|
+
// Skip collision checks if no collision mask
|
|
351
|
+
if (actorMask === 0) {
|
|
352
|
+
// Just move without checking collisions
|
|
353
|
+
this._xRemainder -= move;
|
|
354
|
+
this._x += move;
|
|
355
|
+
this.updateView();
|
|
356
|
+
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this._xRemainder -= move;
|
|
361
|
+
const sign = Math.sign(move);
|
|
362
|
+
let remaining = Math.abs(move);
|
|
363
|
+
const step = sign;
|
|
364
|
+
|
|
365
|
+
// If we're being pushed by a solid, temporarily make it non-collidable
|
|
366
|
+
if (pushingSolid) {
|
|
367
|
+
pushingSolid.collideable = false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Move one pixel at a time, checking for collisions
|
|
371
|
+
while (remaining > 0) {
|
|
372
|
+
const nextX = this._x + step;
|
|
373
|
+
|
|
374
|
+
// Get solids at the next position
|
|
375
|
+
const solids = this.getSolidsAt(nextX, this._y);
|
|
376
|
+
let collided = false;
|
|
377
|
+
|
|
378
|
+
// Check for collisions with each solid
|
|
379
|
+
for (const solid of solids) {
|
|
380
|
+
// Skip if solid can't collide
|
|
381
|
+
if (!solid.canCollide) continue;
|
|
382
|
+
|
|
383
|
+
// Skip if collision layers don't match
|
|
384
|
+
if ((actorLayer & solid.collisionMask) === 0 || (solid.collisionLayer & actorMask) === 0) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Calculate collision details
|
|
389
|
+
const result: CollisionResult = {
|
|
390
|
+
collided: true,
|
|
391
|
+
solid,
|
|
392
|
+
normal: { x: -sign, y: 0 },
|
|
393
|
+
penetration: step > 0 ? this.x + this.width - solid.x : solid.x + solid.width - this.x,
|
|
394
|
+
pushingSolid,
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// Add to collisions array
|
|
398
|
+
collisions.push(result);
|
|
399
|
+
|
|
400
|
+
// Call collision handler if provided
|
|
401
|
+
if (collisionHandler) {
|
|
402
|
+
collisionHandler(result);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Call actor's collision handler
|
|
406
|
+
this.onCollide(result);
|
|
407
|
+
|
|
408
|
+
collided = true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (collided) {
|
|
412
|
+
// Stop movement on collision
|
|
413
|
+
break;
|
|
414
|
+
} else {
|
|
415
|
+
// Move to next position
|
|
416
|
+
this._x = nextX;
|
|
417
|
+
remaining--;
|
|
418
|
+
|
|
419
|
+
// Update view every few pixels for better performance
|
|
420
|
+
// This reduces the number of view updates during movement
|
|
421
|
+
if (remaining % 4 === 0 || remaining === 0) {
|
|
422
|
+
this.updateView();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Restore solid's collidable state
|
|
428
|
+
if (pushingSolid) {
|
|
429
|
+
pushingSolid.collideable = true;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Final view update if we moved
|
|
433
|
+
if (Math.abs(move) - remaining > 0) {
|
|
434
|
+
this.updateView();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return collisions;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Moves the actor vertically, checking for collisions with solids.
|
|
442
|
+
*
|
|
443
|
+
* @param amount - Distance to move in pixels
|
|
444
|
+
* @param collisionHandler - Optional callback for handling collisions
|
|
445
|
+
* @returns Array of collision results
|
|
446
|
+
*/
|
|
447
|
+
public moveY(
|
|
448
|
+
amount: number,
|
|
449
|
+
collisionHandler?: (result: CollisionResult) => void,
|
|
450
|
+
pushingSolid?: Solid,
|
|
451
|
+
): CollisionResult[] {
|
|
452
|
+
// Early return if inactive or zero movement
|
|
453
|
+
if (!this.active || amount === 0) return [];
|
|
454
|
+
|
|
455
|
+
this._yRemainder += amount;
|
|
456
|
+
const move = Math.round(this._yRemainder);
|
|
457
|
+
|
|
458
|
+
// Early return if rounded movement is zero
|
|
459
|
+
if (move === 0) return [];
|
|
460
|
+
|
|
461
|
+
const collisions: CollisionResult[] = [];
|
|
462
|
+
|
|
463
|
+
// Cache collision layer and mask for faster access
|
|
464
|
+
const actorLayer = this.collisionLayer;
|
|
465
|
+
const actorMask = this.collisionMask;
|
|
466
|
+
|
|
467
|
+
// Skip collision checks if no collision mask
|
|
468
|
+
if (actorMask === 0) {
|
|
469
|
+
// Just move without checking collisions
|
|
470
|
+
this._yRemainder -= move;
|
|
471
|
+
this._y += move;
|
|
472
|
+
this.updateView();
|
|
473
|
+
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
this._yRemainder -= move;
|
|
478
|
+
const sign = Math.sign(move);
|
|
479
|
+
let remaining = Math.abs(move);
|
|
480
|
+
const step = sign;
|
|
481
|
+
|
|
482
|
+
// If we're being pushed by a solid, temporarily make it non-collidable
|
|
483
|
+
if (pushingSolid) {
|
|
484
|
+
pushingSolid.collideable = false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Move one pixel at a time, checking for collisions
|
|
488
|
+
while (remaining > 0) {
|
|
489
|
+
const nextY = this._y + step;
|
|
490
|
+
|
|
491
|
+
// Get solids at the next position
|
|
492
|
+
const solids = this.getSolidsAt(this._x, nextY);
|
|
493
|
+
let collided = false;
|
|
494
|
+
|
|
495
|
+
// Check for collisions with each solid
|
|
496
|
+
for (const solid of solids) {
|
|
497
|
+
// Skip if solid can't collide
|
|
498
|
+
if (!solid.canCollide) continue;
|
|
499
|
+
|
|
500
|
+
// Skip if collision layers don't match
|
|
501
|
+
if ((actorLayer & solid.collisionMask) === 0 || (solid.collisionLayer & actorMask) === 0) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Calculate collision details
|
|
506
|
+
const result: CollisionResult = {
|
|
507
|
+
collided: true,
|
|
508
|
+
solid,
|
|
509
|
+
normal: { x: 0, y: -sign },
|
|
510
|
+
penetration: step > 0 ? this.y + this.height - solid.y : solid.y + solid.height - this.y,
|
|
511
|
+
pushingSolid,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// Add to collisions array
|
|
515
|
+
collisions.push(result);
|
|
516
|
+
|
|
517
|
+
// Call collision handler if provided
|
|
518
|
+
if (collisionHandler) {
|
|
519
|
+
collisionHandler(result);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Call actor's collision handler
|
|
523
|
+
this.onCollide(result);
|
|
524
|
+
|
|
525
|
+
collided = true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (collided) {
|
|
529
|
+
// Stop movement on collision
|
|
530
|
+
break;
|
|
531
|
+
} else {
|
|
532
|
+
// Move to next position
|
|
533
|
+
this._y = nextY;
|
|
534
|
+
remaining--;
|
|
535
|
+
|
|
536
|
+
// Update view every few pixels for better performance
|
|
537
|
+
// This reduces the number of view updates during movement
|
|
538
|
+
if (remaining % 4 === 0 || remaining === 0) {
|
|
539
|
+
this.updateView();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Restore solid's collidable state
|
|
545
|
+
if (pushingSolid) {
|
|
546
|
+
pushingSolid.collideable = true;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Final view update if we moved
|
|
550
|
+
if (Math.abs(move) - remaining > 0) {
|
|
551
|
+
this.updateView();
|
|
552
|
+
|
|
553
|
+
// Update grid cells if actor moved and actor collisions are enabled
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return collisions;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Updates the actor's view position.
|
|
561
|
+
*/
|
|
562
|
+
public updateView(): void {
|
|
563
|
+
if (this.view && this.view.visible) {
|
|
564
|
+
this.view.x = this._x;
|
|
565
|
+
this.view.y = this._y;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Gets all solids at the specified position that could collide with this actor.
|
|
571
|
+
*
|
|
572
|
+
* @param _x - X position to check
|
|
573
|
+
* @param _y - Y position to check
|
|
574
|
+
* @returns Array of solids at the position
|
|
575
|
+
*/
|
|
576
|
+
protected getSolidsAt(_x: number, _y: number): Solid[] {
|
|
577
|
+
return this.system.getSolidsAt(_x, _y, this);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Checks if this actor is colliding with another actor.
|
|
582
|
+
* The collision will only occur if:
|
|
583
|
+
* 1. Both actors are active
|
|
584
|
+
* 2. Neither actor has disabled actor collisions
|
|
585
|
+
* 3. The collision layers and masks match:
|
|
586
|
+
* - (this.collisionLayer & other.collisionMask) !== 0
|
|
587
|
+
* - (other.collisionLayer & this.collisionMask) !== 0
|
|
588
|
+
*
|
|
589
|
+
* @param actor - The actor to check collision with
|
|
590
|
+
* @returns Collision result with information about the collision
|
|
591
|
+
*/
|
|
592
|
+
public checkActorCollision(actor: Actor): ActorCollisionResult {
|
|
593
|
+
// Skip if either actor is not active
|
|
594
|
+
if (!this.active || !actor.active) {
|
|
595
|
+
return { collided: false, actor };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Skip if either actor has disabled actor collisions
|
|
599
|
+
if (this.disableActorCollisions || actor.disableActorCollisions) {
|
|
600
|
+
return { collided: false, actor };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Skip if the actors can't collide based on collision layers
|
|
604
|
+
if ((this.collisionLayer & actor.collisionMask) === 0 || (actor.collisionLayer & this.collisionMask) === 0) {
|
|
605
|
+
return { collided: false, actor };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Simple AABB collision check
|
|
609
|
+
const thisLeft = this.x;
|
|
610
|
+
const thisRight = this.x + this.width;
|
|
611
|
+
const thisTop = this.y;
|
|
612
|
+
const thisBottom = this.y + this.height;
|
|
613
|
+
|
|
614
|
+
const otherLeft = actor.x;
|
|
615
|
+
const otherRight = actor.x + actor.width;
|
|
616
|
+
const otherTop = actor.y;
|
|
617
|
+
const otherBottom = actor.y + actor.height;
|
|
618
|
+
|
|
619
|
+
// Check if the bounding boxes overlap
|
|
620
|
+
if (thisRight > otherLeft && thisLeft < otherRight && thisBottom > otherTop && thisTop < otherBottom) {
|
|
621
|
+
// Calculate penetration and normal
|
|
622
|
+
const overlapX = Math.min(thisRight - otherLeft, otherRight - thisLeft);
|
|
623
|
+
const overlapY = Math.min(thisBottom - otherTop, otherBottom - thisTop);
|
|
624
|
+
|
|
625
|
+
let normal: Vector2;
|
|
626
|
+
let penetration: number;
|
|
627
|
+
|
|
628
|
+
// Determine the collision normal based on the smallest overlap
|
|
629
|
+
if (overlapX < overlapY) {
|
|
630
|
+
penetration = overlapX;
|
|
631
|
+
normal = {
|
|
632
|
+
x: thisLeft < otherLeft ? -1 : 1,
|
|
633
|
+
y: 0,
|
|
634
|
+
};
|
|
635
|
+
} else {
|
|
636
|
+
penetration = overlapY;
|
|
637
|
+
normal = {
|
|
638
|
+
x: 0,
|
|
639
|
+
y: thisTop < otherTop ? -1 : 1,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
collided: true,
|
|
645
|
+
actor,
|
|
646
|
+
normal,
|
|
647
|
+
penetration,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return { collided: false, actor };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Resolves a collision with another actor.
|
|
656
|
+
*
|
|
657
|
+
* @param result - The collision result to resolve
|
|
658
|
+
* @param shouldMove - Whether this actor should move to resolve the collision
|
|
659
|
+
* @returns The updated collision result
|
|
660
|
+
*/
|
|
661
|
+
public resolveActorCollision(result: ActorCollisionResult): ActorCollisionResult {
|
|
662
|
+
if (!result.collided || !result.normal || !result.penetration) {
|
|
663
|
+
return result;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Call the collision handler
|
|
667
|
+
this.onActorCollide(result);
|
|
668
|
+
|
|
669
|
+
// Example:
|
|
670
|
+
// Move this actor to resolve the collision
|
|
671
|
+
// this.x += result.normal.x * result.penetration * 0.5;
|
|
672
|
+
// this.y += result.normal.y * result.penetration * 0.5;
|
|
673
|
+
|
|
674
|
+
return result;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Sets the actor's size and updates grid cells if needed.
|
|
679
|
+
*
|
|
680
|
+
* @param width - New width in pixels
|
|
681
|
+
* @param height - New height in pixels
|
|
682
|
+
*/
|
|
683
|
+
public setSize(width: number, height: number): void {
|
|
684
|
+
const sizeChanged = this.width !== width || this.height !== height;
|
|
685
|
+
|
|
686
|
+
this.width = width;
|
|
687
|
+
this.height = height;
|
|
688
|
+
|
|
689
|
+
// Update grid cells if size changed and actor collisions are enabled
|
|
690
|
+
if (sizeChanged && this.system.enableActorCollisions) {
|
|
691
|
+
this.updateGridCells();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Sets the actor's width and updates grid cells if needed.
|
|
697
|
+
*
|
|
698
|
+
* @param value - New width in pixels
|
|
699
|
+
*/
|
|
700
|
+
public setWidth(value: number): void {
|
|
701
|
+
if (this.width !== value) {
|
|
702
|
+
this.width = value;
|
|
703
|
+
|
|
704
|
+
// Update grid cells if size changed and actor collisions are enabled
|
|
705
|
+
if (this.system.enableActorCollisions) {
|
|
706
|
+
this.updateGridCells();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Sets the actor's height and updates grid cells if needed.
|
|
713
|
+
*
|
|
714
|
+
* @param value - New height in pixels
|
|
715
|
+
*/
|
|
716
|
+
public setHeight(value: number): void {
|
|
717
|
+
if (this.height !== value) {
|
|
718
|
+
this.height = value;
|
|
719
|
+
|
|
720
|
+
// Update grid cells if size changed and actor collisions are enabled
|
|
721
|
+
if (this.system.enableActorCollisions) {
|
|
722
|
+
this.updateGridCells();
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|