@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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * This file provides TypeScript declaration merging for custom collision layers.
3
+ *
4
+ * To use this feature, create a declaration file in your project (e.g., collision-layers.d.ts)
5
+ * and extend the UserCollisionLayers interface with your custom layers:
6
+ *
7
+ * ```typescript
8
+ * // collision-layers.d.ts in your project
9
+ * declare module '@caperjs/plugin-crunch' {
10
+ * interface UserCollisionLayers {
11
+ * WATER: number;
12
+ * LAVA: number;
13
+ * CLOUD: number;
14
+ * }
15
+ * }
16
+ * ```
17
+ *
18
+ * Then you can access your custom layers with intellisense:
19
+ *
20
+ * ```typescript
21
+ * import { physics } from '@caperjs/core';
22
+ *
23
+ * // Register your custom layers
24
+ * physics.registerCollisionLayer('WATER', 0);
25
+ * physics.registerCollisionLayer('LAVA', 1);
26
+ * physics.registerCollisionLayer('CLOUD', 2);
27
+ *
28
+ * // Access with intellisense
29
+ * const waterLayer = physics.getCollisionLayer('WATER');
30
+ * ```
31
+ */
32
+
33
+ /**
34
+ * Interface for user-defined collision layers.
35
+ * Extend this interface in your own declaration files to add intellisense support
36
+ * for your custom collision layers.
37
+ */
38
+ export interface UserCollisionLayers {
39
+ // This is intentionally empty and should be extended by users
40
+ }
41
+
42
+ /**
43
+ * Extended plugin interface with intellisense support for custom collision layers
44
+ */
45
+ export interface CollisionLayerPluginExtensions {
46
+ /**
47
+ * Gets a registered collision layer by name with intellisense support.
48
+ *
49
+ * @param name Name of the collision layer
50
+ * @returns The numeric value of the registered collision layer or undefined if not found
51
+ */
52
+ getCollisionLayer<K extends keyof UserCollisionLayers>(name: K): number | undefined;
53
+
54
+ /**
55
+ * Registers a named collision layer with intellisense support.
56
+ *
57
+ * @param name Name of the collision layer
58
+ * @param index Index from 0-15 representing which user bit to use
59
+ * @param description Optional description of the layer
60
+ * @returns The numeric value of the registered collision layer
61
+ */
62
+ registerCollisionLayer<K extends keyof UserCollisionLayers>(name: K, index: number, description?: string): number;
63
+
64
+ /**
65
+ * Registers a named collision layer with automatic index assignment and intellisense support.
66
+ *
67
+ * @param name Name of the collision layer
68
+ * @param description Optional description of the layer
69
+ * @returns The numeric value of the registered collision layer
70
+ */
71
+ registerCollisionLayer<K extends keyof UserCollisionLayers>(name: K, description?: string): number;
72
+
73
+ /**
74
+ * Removes a registered collision layer with intellisense support.
75
+ *
76
+ * @param name Name of the collision layer to remove
77
+ * @returns True if the layer was removed, false if it didn't exist
78
+ */
79
+ removeCollisionLayer<K extends keyof UserCollisionLayers>(name: K): boolean;
80
+ }
81
+
82
+ // Extend the ICrunchPhysicsPlugin interface to include the CollisionLayerPluginExtensions
83
+ import './CrunchPhysicsPlugin';
84
+
85
+ declare module './CrunchPhysicsPlugin' {
86
+ interface ICrunchPhysicsPlugin extends CollisionLayerPluginExtensions {}
87
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ import CrunchPhysicsPlugin from './CrunchPhysicsPlugin';
2
+
3
+ export * from './Actor';
4
+ // Don't export from collision-layers.d.ts as it's a declaration file
5
+ export * from './Entity';
6
+ export * from './Group';
7
+ export * from './interfaces';
8
+ export * from './Sensor';
9
+ export * from './Solid';
10
+ export * from './System';
11
+ export * from './types';
12
+
13
+ export type { ICrunchPhysicsPlugin } from './CrunchPhysicsPlugin';
14
+
15
+ // Export specific types for easier access
16
+ export * from './types';
17
+
18
+ export default CrunchPhysicsPlugin;
@@ -0,0 +1,33 @@
1
+ import { Container, Rectangle } from 'pixi.js';
2
+ import { ActorCollision, Collision, SensorOverlap } from './types';
3
+
4
+ export interface CrunchPhysicsOptions {
5
+ container: Container;
6
+ /** Grid cell size in pixels */
7
+ gridSize?: number;
8
+ /** Gravity strength */
9
+ gravity?: number;
10
+ /** Maximum velocity */
11
+ maxVelocity?: number;
12
+ /** Whether to enable debug rendering */
13
+ debug?: boolean;
14
+ /** Whether to cull out-of-bounds entities */
15
+ culling?: boolean;
16
+ /** Whether to remove entities from the system after culling */
17
+ boundary?: Rectangle;
18
+ /** Collision resolver */
19
+ collisionResolver?: (collisions: Collision[]) => void;
20
+ /** Overlap resolver */
21
+ overlapResolver?: (overlaps: SensorOverlap[]) => void;
22
+ /** Actor-to-actor collision resolver */
23
+ actorCollisionResolver?: (collisions: ActorCollision[]) => void;
24
+ /** Whether to enable actor-to-actor collisions */
25
+ enableActorCollisions?: boolean;
26
+ }
27
+
28
+ export interface AABBLike {
29
+ x: number;
30
+ y: number;
31
+ width: number;
32
+ height: number;
33
+ }
package/src/types.ts ADDED
@@ -0,0 +1,336 @@
1
+ import { PointLike, SizeLike } from '@caperjs/core';
2
+ import { Container } from 'pixi.js';
3
+ import { Actor } from './Actor';
4
+ import { Entity } from './Entity';
5
+ import { Group } from './Group';
6
+ import { Sensor } from './Sensor';
7
+ import { Solid } from './Solid';
8
+
9
+ export interface Vector2 {
10
+ x: number;
11
+ y: number;
12
+ }
13
+
14
+ export interface Rectangle {
15
+ x: number;
16
+ y: number;
17
+ width: number;
18
+ height: number;
19
+ }
20
+
21
+ export type CollisionShape = 'rectangle';
22
+ export type EntityData = {
23
+ [key: string]: any;
24
+ };
25
+
26
+ export type PhysicsEntityClass = new (config?: PhysicsEntityConfig) => Actor | Solid | Sensor;
27
+
28
+ /**
29
+ * Collision layers using bitwise flags
30
+ * Each entity can belong to multiple layers (using bitwise OR)
31
+ * and can collide with multiple layers (using bitwise AND)
32
+ */
33
+ export enum CollisionLayer {
34
+ NONE = 0,
35
+ DEFAULT = 1 << 0,
36
+ PLAYER = 1 << 1,
37
+ ENEMY = 1 << 2,
38
+ PROJECTILE = 1 << 3,
39
+ PLATFORM = 1 << 4,
40
+ TRIGGER = 1 << 5,
41
+ ITEM = 1 << 6,
42
+ WALL = 1 << 7,
43
+ FX = 1 << 8,
44
+ // Reserve first 16 bits for built-in layers
45
+ // Bits 16-31 are available for user-defined layers
46
+ ALL = 0xffffffff, // All bits set to 1
47
+ }
48
+
49
+ /**
50
+ * Interface for a registered collision layer
51
+ */
52
+ export interface RegisteredCollisionLayer {
53
+ /** Name of the collision layer */
54
+ name: string;
55
+ /** Numeric value of the collision layer (bitwise) */
56
+ value: number;
57
+ /** Description of the collision layer (optional) */
58
+ description?: string;
59
+ }
60
+
61
+ /**
62
+ * Registry for tracking custom collision layers
63
+ */
64
+ export class CollisionLayerRegistry {
65
+ private static _instance: CollisionLayerRegistry;
66
+ private _layers: Map<string, RegisteredCollisionLayer> = new Map();
67
+ private _usedIndices: Set<number> = new Set();
68
+
69
+ /**
70
+ * Get the singleton instance of the registry
71
+ */
72
+ public static get instance(): CollisionLayerRegistry {
73
+ if (!CollisionLayerRegistry._instance) {
74
+ CollisionLayerRegistry._instance = new CollisionLayerRegistry();
75
+ }
76
+ return CollisionLayerRegistry._instance;
77
+ }
78
+
79
+ /**
80
+ * Register a new collision layer
81
+ *
82
+ * @param name Name of the collision layer
83
+ * @param index Index from 0-15 representing which user bit to use (gets shifted to bits 16-31)
84
+ * @param description Optional description of the layer
85
+ * @returns The registered collision layer
86
+ */
87
+ public register(name: string, index: number, description?: string): RegisteredCollisionLayer {
88
+ if (index < 0 || index > 15) {
89
+ throw new Error('Custom collision layer index must be between 0 and 15');
90
+ }
91
+
92
+ if (this._usedIndices.has(index)) {
93
+ throw new Error(`Collision layer index ${index} is already in use`);
94
+ }
95
+
96
+ if (this._layers.has(name)) {
97
+ throw new Error(`Collision layer with name "${name}" already exists`);
98
+ }
99
+
100
+ const value = 1 << (index + 16);
101
+ const layer: RegisteredCollisionLayer = { name, value, description };
102
+
103
+ this._layers.set(name, layer);
104
+ this._usedIndices.add(index);
105
+
106
+ return layer;
107
+ }
108
+
109
+ /**
110
+ * Get a registered collision layer by name
111
+ *
112
+ * @param name Name of the collision layer
113
+ * @returns The registered collision layer or undefined if not found
114
+ */
115
+ public get(name: string): RegisteredCollisionLayer | undefined {
116
+ return this._layers.get(name);
117
+ }
118
+
119
+ /**
120
+ * Get all registered collision layers
121
+ *
122
+ * @returns Array of all registered collision layers
123
+ */
124
+ public getAll(): RegisteredCollisionLayer[] {
125
+ return Array.from(this._layers.values());
126
+ }
127
+
128
+ /**
129
+ * Check if a collision layer with the given name exists
130
+ *
131
+ * @param name Name of the collision layer
132
+ * @returns True if the layer exists
133
+ */
134
+ public has(name: string): boolean {
135
+ return this._layers.has(name);
136
+ }
137
+
138
+ /**
139
+ * Remove a registered collision layer
140
+ *
141
+ * @param name Name of the collision layer to remove
142
+ * @returns True if the layer was removed, false if it didn't exist
143
+ */
144
+ public remove(name: string): boolean {
145
+ const layer = this._layers.get(name);
146
+ if (!layer) return false;
147
+
148
+ // Calculate the index from the value
149
+ const value = layer.value;
150
+ const index = Math.log2(value) - 16;
151
+
152
+ this._usedIndices.delete(index);
153
+ return this._layers.delete(name);
154
+ }
155
+
156
+ /**
157
+ * Clear all registered collision layers
158
+ */
159
+ public clear(): void {
160
+ this._layers.clear();
161
+ this._usedIndices.clear();
162
+ }
163
+
164
+ /**
165
+ * Get the next available index for a custom collision layer
166
+ *
167
+ * @returns The next available index or -1 if all indices are used
168
+ */
169
+ public getNextAvailableIndex(): number {
170
+ for (let i = 0; i < 16; i++) {
171
+ if (!this._usedIndices.has(i)) {
172
+ return i;
173
+ }
174
+ }
175
+ return -1;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Utility functions for working with collision layers
181
+ */
182
+ export const CollisionLayers = {
183
+ /**
184
+ * Creates a custom collision layer using bits 16-31 (user space)
185
+ *
186
+ * @param index Index from 0-15 representing which user bit to use (gets shifted to bits 16-31)
187
+ * @returns A unique collision layer value
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * // Create custom collision layers
192
+ * const WATER_LAYER = CollisionLayers.createLayer(0); // 1 << 16
193
+ * const LAVA_LAYER = CollisionLayers.createLayer(1); // 1 << 17
194
+ * const CLOUD_LAYER = CollisionLayers.createLayer(2); // 1 << 18
195
+ *
196
+ * // Use in entity creation
197
+ * const waterEntity = physics.createActor({
198
+ * type: 'Water',
199
+ * position: [100, 400],
200
+ * size: [800, 100],
201
+ * collisionLayer: WATER_LAYER,
202
+ * collisionMask: CollisionLayer.PLAYER | CollisionLayer.ENEMY
203
+ * });
204
+ * ```
205
+ */
206
+ createLayer(index: number): number {
207
+ if (index < 0 || index > 15) {
208
+ throw new Error('Custom collision layer index must be between 0 and 15');
209
+ }
210
+ return 1 << (index + 16);
211
+ },
212
+
213
+ /**
214
+ * Creates a collision mask from multiple layers
215
+ *
216
+ * @param layers Array of collision layers to combine
217
+ * @returns A combined collision mask
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * // Create a mask that collides with players, enemies and projectiles
222
+ * const mask = CollisionLayers.createMask([
223
+ * CollisionLayer.PLAYER,
224
+ * CollisionLayer.ENEMY,
225
+ * CollisionLayer.PROJECTILE
226
+ * ]);
227
+ * ```
228
+ */
229
+ createMask(layers: number[]): number {
230
+ return layers.reduce((mask, layer) => mask | layer, 0);
231
+ },
232
+
233
+ /**
234
+ * Checks if a layer is included in a mask
235
+ *
236
+ * @param layer The layer to check
237
+ * @param mask The mask to check against
238
+ * @returns True if the layer is included in the mask
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * // Check if player layer is in the mask
243
+ * if (CollisionLayers.isLayerInMask(CollisionLayer.PLAYER, entity.collisionMask)) {
244
+ * console.log('Entity can collide with players');
245
+ * }
246
+ * ```
247
+ */
248
+ isLayerInMask(layer: number, mask: number): boolean {
249
+ return (layer & mask) !== 0;
250
+ },
251
+
252
+ /**
253
+ * Get the registry for custom collision layers
254
+ *
255
+ * @returns The collision layer registry
256
+ */
257
+ getRegistry(): CollisionLayerRegistry {
258
+ return CollisionLayerRegistry.instance;
259
+ },
260
+ };
261
+
262
+ export interface PhysicsEntityConfig<D extends EntityData = EntityData> {
263
+ id?: string;
264
+ class?: PhysicsEntityClass;
265
+ type?: PhysicsEntityType;
266
+ position?: PointLike;
267
+ size?: SizeLike;
268
+ x?: number;
269
+ y?: number;
270
+ width?: number;
271
+ height?: number;
272
+ restitution?: number;
273
+ view?: PhysicsEntityView;
274
+ data?: Partial<D>;
275
+ group?: Group;
276
+ groupOffset?: PointLike;
277
+ follows?: Entity;
278
+ followOffset?: PointLike;
279
+ /** Collision layer this entity belongs to (bitwise) */
280
+ collisionLayer?: number;
281
+ /** Collision mask defining which layers this entity collides with (bitwise) */
282
+ collisionMask?: number;
283
+ /** Whether to disable actor-to-actor collisions for this entity */
284
+ disableActorCollisions?: boolean;
285
+ }
286
+
287
+ export interface CollisionResult {
288
+ collided: boolean;
289
+ normal?: Vector2;
290
+ penetration?: number;
291
+ solid: Solid;
292
+ }
293
+
294
+ export interface SensorOverlap {
295
+ type: `${PhysicsEntityType}|${PhysicsEntityType}`;
296
+ actor: Actor;
297
+ sensor: Sensor;
298
+ }
299
+
300
+ export interface CollisionResult {
301
+ collided: boolean;
302
+ normal?: Vector2;
303
+ penetration?: number;
304
+ solid: Solid;
305
+ pushingSolid?: Solid;
306
+ }
307
+
308
+ /**
309
+ * Result of an actor-to-actor collision
310
+ */
311
+ export interface ActorCollisionResult {
312
+ collided: boolean;
313
+ normal?: Vector2;
314
+ penetration?: number;
315
+ actor: Actor;
316
+ }
317
+
318
+ export interface Collision {
319
+ type: `${PhysicsEntityType}|${PhysicsEntityType}`;
320
+ entity1: Actor | Sensor;
321
+ entity2: Actor | Solid;
322
+ result: CollisionResult;
323
+ }
324
+
325
+ /**
326
+ * Represents a collision between two actors
327
+ */
328
+ export interface ActorCollision {
329
+ type: `${PhysicsEntityType}|${PhysicsEntityType}`;
330
+ actor1: Actor;
331
+ actor2: Actor;
332
+ result: ActorCollisionResult;
333
+ }
334
+
335
+ export type PhysicsEntityView = Container;
336
+ export type PhysicsEntityType = 'Actor' | 'Solid' | 'Sensor' | 'Group' | string;
package/src/utils.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { resolvePointLike, resolveSizeLike } from '@caperjs/core';
2
+ import { PhysicsEntityConfig } from './types';
3
+
4
+ /**
5
+ * Utility functions for resolving entity positions and sizes from various input formats.
6
+ * These functions handle the conversion of different coordinate and size specifications
7
+ * into standardized formats used by the physics system.
8
+ */
9
+
10
+ /**
11
+ * Resolves an entity's position from various input formats.
12
+ * Supports direct x/y values or a position object/array.
13
+ *
14
+ * @param config - Entity configuration containing position information
15
+ * @returns Resolved {x, y} coordinates
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * // Using direct x/y values
20
+ * resolveEntityPosition({ x: 100, y: 200 })
21
+ * // → { x: 100, y: 200 }
22
+ *
23
+ * // Using position array
24
+ * resolveEntityPosition({ position: [100, 200] })
25
+ * // → { x: 100, y: 200 }
26
+ *
27
+ * // Using position object
28
+ * resolveEntityPosition({ position: { x: 100, y: 200 } })
29
+ * // → { x: 100, y: 200 }
30
+ * ```
31
+ */
32
+ export function resolveEntityPosition(config: PhysicsEntityConfig): { x: number; y: number } {
33
+ const { x, y } =
34
+ config.position !== undefined ? resolvePointLike(config.position) : { x: config?.x ?? 0, y: config?.y ?? 0 };
35
+
36
+ return { x, y };
37
+ }
38
+
39
+ /**
40
+ * Resolves an entity's size from various input formats.
41
+ * Supports direct width/height values or a size object/array.
42
+ *
43
+ * @param config - Entity configuration containing size information
44
+ * @returns Resolved {width, height} dimensions
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * // Using direct width/height values
49
+ * resolveEntitySize({ width: 100, height: 200 })
50
+ * // → { width: 100, height: 200 }
51
+ *
52
+ * // Using size array
53
+ * resolveEntitySize({ size: [100, 200] })
54
+ * // → { width: 100, height: 200 }
55
+ *
56
+ * // Using size object
57
+ * resolveEntitySize({ size: { width: 100, height: 200 } })
58
+ * // → { width: 100, height: 200 }
59
+ *
60
+ * // Using defaults
61
+ * resolveEntitySize({})
62
+ * // → { width: 32, height: 32 }
63
+ * ```
64
+ */
65
+ export function resolveEntitySize(config: PhysicsEntityConfig): { width: number; height: number } {
66
+ const { width, height } =
67
+ config.size !== undefined
68
+ ? resolveSizeLike(config.size)
69
+ : { width: config?.width ?? 32, height: config?.height ?? 32 };
70
+
71
+ return { width, height };
72
+ }
package/src/version.ts ADDED
@@ -0,0 +1 @@
1
+ export const version = '0.1.0';