UNPKG

@dill-pixel/plugin-crunch-physics

Version:

Crunch Physics

719 lines (650 loc) 20.2 kB
import { IApplication, IPlugin, isDev, Logger, Plugin, version } from 'dill-pixel'; import { Container as PIXIContainer, Rectangle, Ticker } from 'pixi.js'; import { Actor } from './Actor'; import { Group } from './Group'; import { AABBLike, CrunchPhysicsOptions } from './interfaces'; import { Sensor } from './Sensor'; import { Solid } from './Solid'; import { System } from './System'; import { ActorCollision, Collision, CollisionLayer, CollisionLayers, PhysicsEntityConfig, RegisteredCollisionLayer, SensorOverlap, } from './types'; /** * Interface for the Crunch physics plugin, providing a simple yet powerful 2D physics system * inspired by Towerfall's physics mechanics. Supports dynamic actors, static solids, and trigger sensors. * * @example * ```typescript * // Initialize the physics system * const physics = this.app.getPlugin('crunch-physics') as ICrunchPhysicsPlugin; * * const physics = await this.physics.initialize({ * gravity: 900, * debug: true, * gridSize: 32 * }); * * // Create a player character * const playerSprite = this.add.sprite({asset:Texture.WHITE, width: 32, height: 64}); * * const player = physics.createActor({ * type: 'Player', * position: [100, 100], * size: [32, 64], * view: playerSprite * }); * * // Create a platform * const platformSprite = this.add.sprite({asset:Texture.WHITE, width: 800, height: 32, tint: 0x00FF00}); * const platform = physics.createSolid({ * type: 'Platform', * position: [0, 500], * size: [800, 32], * view: platformSprite * }); * * // Create a coin pickup sensor * const coinSprite = this.add.sprite({asset:Texture.WHITE, width: 32, height: 32, tint: 0x00FF00}); * const coin = physics.createSensor({ * type: 'Coin', * position: [400, 400], * size: [32, 32], * view: coinSprite * }); * ``` */ export interface ICrunchPhysicsPlugin extends IPlugin<CrunchPhysicsOptions> { system: System; container: PIXIContainer; enabled: boolean; /** * Initializes the physics system with the specified options. */ initialize(options?: Partial<CrunchPhysicsOptions>, app?: IApplication): Promise<void>; /** * Sets a custom collision resolver function that will be called when collisions occur. * * @param resolver - Function to handle collisions * * @example * ```typescript * physics.setCollisionResolver((collisions) => { * collisions.forEach(collision => { * if (collision.actor.type === 'Player' && collision.solid.type === 'Spike') { * player.damage(10); * } * }); * }); * ``` */ setCollisionResolver(resolver: (collisions: Collision[]) => void): void; /** * Sets a custom resolver for actor-to-actor collisions. * * @param resolver - Function to handle actor-to-actor collisions * * @example * ```typescript * physics.setActorCollisionResolver((collisions) => { * collisions.forEach(collision => { * if (collision.actor1.type === 'Player' && collision.actor2.type === 'Enemy') { * player.takeDamage(10); * } * }); * }); * ``` */ setActorCollisionResolver(resolver: (collisions: ActorCollision[]) => void): void; /** * Enables or disables actor-to-actor collision detection. * * @param enabled - Whether to enable actor-to-actor collisions * * @example * ```typescript * // Enable actor-to-actor collisions * physics.setActorCollisionsEnabled(true); * * // Disable actor-to-actor collisions for performance * physics.setActorCollisionsEnabled(false); * ``` */ setActorCollisionsEnabled(enabled: boolean): void; /** * Creates a generic physics entity based on the provided configuration. * Use this when you need to create an entity without knowing its specific type at compile time. * * @param config - Configuration for the physics entity * @returns The created physics entity * * @example * ```typescript * const entityConfig = { * type: 'Platform', * position: [100, 100], * size: [200, 32], * view: sprite * }; * const entity = physics.createEntity(entityConfig); * ``` */ createEntity(config: PhysicsEntityConfig): Actor | Solid | Sensor | Group; /** * Adds an existing physics entity to the system. * Useful when you want to manage entity creation yourself. * * @param entity - The physics entity to add * @returns The added entity * * @example * ```typescript * class CustomActor extends Actor { * constructor() { * super({ type: 'Custom', position: [0, 0], size: [32, 32] }); * } * } * * const customActor = new CustomActor(); * physics.addEntity(customActor); * ``` */ addEntity(entity: Actor | Solid | Sensor | Group): Actor | Solid | Sensor | Group; /** * Creates a dynamic physics actor that can move and collide with other entities. * Actors are typically used for players, enemies, or any moving game objects. * * @param config - Configuration for the actor * @returns The created actor * * @example * ```typescript * // Create a player character * const player = physics.createActor({ * type: 'Player', * position: [100, 100], * size: [32, 64], * view: playerSprite * }); * * // Update player in game loop * player.velocity.x = 300; // Move right * player.velocity.y = -600; // Jump * ``` */ createActor(config: PhysicsEntityConfig): Actor; /** * Creates a static solid object that other entities can collide with. * Solids are typically used for platforms, walls, and other immovable objects. * * @param config - Configuration for the solid * @returns The created solid * * @example * ```typescript * // Create a static platform * const platform = physics.createSolid({ * type: 'Platform', * position: [0, 500], * size: [800, 32], * view: platformSprite * }); * * // Create a moving platform * const movingPlatform = physics.createSolid({ * type: 'Platform', * position: [100, 300], * size: [200, 32], * view: platformSprite * }); * * // Update platform position * gsap.to(movingPlatform, { * x: 500, * duration: 2, * yoyo: true, * repeat: -1 * }); * ``` */ createSolid(config: PhysicsEntityConfig): Solid; /** * Creates a sensor zone that can detect overlaps with other entities. * Sensors are typically used for triggers, collectibles, or detection zones. * * @param config - Configuration for the sensor * @returns The created sensor * * @example * ```typescript * // Create a coin pickup * const coin = physics.createSensor({ * type: 'Coin', * position: [400, 300], * size: [32, 32], * view: coinSprite * }); * * // Handle coin collection * coin.onActorEnter = (actor) => { * if (actor.type === 'Player') { * increaseScore(10); * physics.removeSensor(coin); * } * }; * ``` */ createSensor(config: PhysicsEntityConfig): Sensor; createGroup(config: PhysicsEntityConfig): Group; addActor(actor: Actor): Actor; addSolid(solid: Solid): Solid; addSensor(sensor: Sensor): Sensor; addGroup(group: Group): Group; /** * Removes an actor from the physics system. * * @param actor - The actor to remove * * @example * ```typescript * // Remove player when they die * function killPlayer(player: Actor) { * playDeathAnimation(); * physics.removeActor(player); * } * ``` */ removeActor(actor: Actor): void; /** * Removes a solid from the physics system. * * @param solid - The solid to remove * * @example * ```typescript * // Remove a platform when it's destroyed * function destroyPlatform(platform: Solid) { * playBreakAnimation(); * physics.removeSolid(platform); * } * ``` */ removeSolid(solid: Solid): void; /** * Removes a sensor from the physics system. * * @param sensor - The sensor to remove * * @example * ```typescript * // Remove a coin when it's collected * function collectCoin(coin: Sensor) { * playCollectSound(); * physics.removeSensor(coin); * } * ``` */ removeSensor(sensor: Sensor): void; /** * Cleans up the physics system and removes all entities. * Call this when transitioning between scenes or shutting down the game. * * @example * ```typescript * class GameScene extends Scene { * destroy() { * this.physics.destroy(); * super.destroy(); * } * } * ``` */ destroy(): void; /** * Creates a custom collision layer. * * @param index Index from 0-15 representing which user bit to use (gets shifted to bits 16-31) * @returns A unique collision layer value * * @example * ```typescript * // Create custom collision layers * const WATER_LAYER = physics.createCollisionLayer(0); * const LAVA_LAYER = physics.createCollisionLayer(1); * * // Use in entity creation * const waterEntity = physics.createActor({ * type: 'Water', * position: [100, 400], * size: [800, 100], * collisionLayer: WATER_LAYER, * collisionMask: CollisionLayer.PLAYER | CollisionLayer.ENEMY * }); * ``` */ createCollisionLayer(index: number): number; /** * Creates a collision mask from multiple layers. * * @param layers Array of collision layers to combine * @returns A combined collision mask * * @example * ```typescript * // Create a mask that collides with players, enemies and projectiles * const mask = physics.createCollisionMask([ * CollisionLayer.PLAYER, * CollisionLayer.ENEMY, * CollisionLayer.PROJECTILE * ]); * ``` */ createCollisionMask(...layers: number[]): number; /** * Registers a named collision layer for better intellisense support. * * @param name Name of the collision layer (used for intellisense) * @param indexOrDescription Index or description of the layer * @param description Optional description of the layer * @returns The numeric value of the registered collision layer * * @example * ```typescript * // Register named collision layers * const WATER = physics.registerCollisionLayer('WATER', 0, 'Water surfaces'); * const LAVA = physics.registerCollisionLayer('LAVA', 1, 'Lava surfaces that damage players'); * * // Use in entity creation * const waterEntity = physics.createActor({ * type: 'Water', * position: [100, 400], * size: [800, 100], * collisionLayer: WATER, * collisionMask: CollisionLayer.PLAYER | CollisionLayer.ENEMY * }); * ``` */ registerCollisionLayer(name: string, indexOrDescription?: number | string, description?: string): number; /** * Gets a registered collision layer by name. * * @param name Name of the collision layer * @returns The numeric value of the registered collision layer or undefined if not found * * @example * ```typescript * // Get a registered collision layer * const waterLayer = physics.getCollisionLayer('WATER'); * if (waterLayer !== undefined) { * // Use the layer * entity.setCollisionLayer(waterLayer); * } * ``` */ getCollisionLayer(name: string): number | undefined; /** * Gets all registered collision layers. * * @returns Array of all registered collision layers * * @example * ```typescript * // Get all registered collision layers * const layers = physics.getCollisionLayers(); * console.log(`Registered layers: ${layers.map(l => l.name).join(', ')}`); * ``` */ getCollisionLayers(): RegisteredCollisionLayer[]; /** * Removes a registered collision layer. * * @param name Name of the collision layer to remove * @returns True if the layer was removed, false if it didn't exist * * @example * ```typescript * // Remove a registered collision layer * physics.removeCollisionLayer('WATER'); * ``` */ removeCollisionLayer(name: string): boolean; /** * Clears all registered collision layers. * * @example * ```typescript * // Clear all registered collision layers * physics.clearCollisionLayers(); * ``` */ clearCollisionLayers(): void; } const defaultOptions: Partial<CrunchPhysicsOptions> = { gridSize: 128, gravity: 900, maxVelocity: 400, debug: false, culling: true, boundary: new Rectangle(0, 0, 800, 600), }; /** * Implementation of the Crunch physics plugin. * See {@link ICrunchPhysicsPlugin} for detailed API documentation and examples. */ export default class CrunchPhysicsPlugin extends Plugin<CrunchPhysicsOptions> implements ICrunchPhysicsPlugin { public system: System; public container: PIXIContainer; public enabled = false; private collisionResolver?: (collisions: Collision[]) => void; private overlapResolver?: (overlaps: SensorOverlap[]) => void; private actorCollisionResolver?: (collisions: ActorCollision[]) => void; constructor() { super('crunch-physics'); } private hello() { const hello = `%c Dill Pixel Crunch Physics Plugin v${version}`; console.log(hello, 'background: rgba(31, 41, 55, 1);color: #74b64c'); } public async initialize(options?: Partial<CrunchPhysicsOptions>, _app: IApplication): Promise<void> { this.hello(); if (!options) { return; } this._options = { ...defaultOptions, ...options } as CrunchPhysicsOptions; this.enabled = true; this.container = options.container || this.app.stage; if (isDev) { Logger.log(this._options); } // Create the physics system const gridSize = options.gridSize ?? 32; const gravity = options.gravity ?? 900; const maxVelocity = options.maxVelocity ?? 400; const debug = options.debug ?? false; const enableActorCollisions = options.enableActorCollisions ?? false; // Create the system with default collision layers set to NONE for better performance this.system = new System({ gridSize, gravity, maxVelocity, boundary: options.boundary, culling: options.culling, plugin: this, collisionResolver: this.collisionResolver, overlapResolver: this.overlapResolver, actorCollisionResolver: this.actorCollisionResolver, enableActorCollisions, defaultCollisionLayer: CollisionLayer.NONE, defaultCollisionMask: CollisionLayer.NONE, }); if (debug) { this.system.debug = true; } // Register update loop this.app.ticker.add(this.update); } public setCollisionResolver(resolver: (collisions: Collision[]) => void): void { this.collisionResolver = resolver; this.system.setCollisionResolver(resolver); } public setActorCollisionResolver(resolver: (collisions: ActorCollision[]) => void): void { this.actorCollisionResolver = resolver; this.system.setActorCollisionResolver(resolver); } public setActorCollisionsEnabled(enabled: boolean): void { this.system.setActorCollisionsEnabled(enabled); } public createEntity(config: PhysicsEntityConfig): Actor | Solid | Sensor | Group { return this.system.createEntity(config); } public addEntity(entity: Actor | Solid | Sensor | Group): Actor | Solid | Sensor | Group { return this.system.addEntity(entity); } public createActor(config: PhysicsEntityConfig): Actor { return this.system.createActor(config); } public createSolid(config: PhysicsEntityConfig): Solid { return this.system.createSolid(config); } public createSensor(config: PhysicsEntityConfig): Sensor { return this.system.createSensor(config); } public createGroup(config: PhysicsEntityConfig): Group { return this.system.createGroup(config); } public addActor(actor: Actor): Actor { return this.system.addActor(actor); } public addSolid(solid: Solid): Solid { return this.system.addSolid(solid); } public addSensor(sensor: Sensor): Sensor { return this.system.addSensor(sensor); } public addGroup(group: Group): Group { return this.system.addGroup(group); } public removeActor(actor: Actor): void { this.system.removeActor(actor); } public removeSolid(solid: Solid): void { this.system.removeSolid(solid); } public removeSensor(sensor: Sensor): void { this.system.removeSensor(sensor); } public removeGroup(group: Group): void { this.system.removeGroup(group); } public destroy(): void { this.enabled = false; this.app.ticker.remove(this.update); if (this.system) { this.system.destroy(); // @ts-expect-error system can't be null this.system = null; } super.destroy(); } private update(_ticker: Ticker) { if (!this.enabled) return; this.system.update(_ticker.deltaTime); } /** * Creates a custom collision layer. * * @param index Index from 0-15 representing which user bit to use (gets shifted to bits 16-31) * @returns A unique collision layer value */ public createCollisionLayer(index: number): number { return CollisionLayers.createLayer(index); } /** * Creates a collision mask from multiple layers. * * @param layers Array of collision layers to combine * @returns A combined collision mask */ public createCollisionMask(...layers: number[]): number { return CollisionLayers.createMask(layers); } /** * Registers a named collision layer for better intellisense support. * * @param name Name of the collision layer (used for intellisense) * @param indexOrDescription Index or description of the layer * @param description Optional description of the layer * @returns The numeric value of the registered collision layer */ public registerCollisionLayer(name: string, indexOrDescription?: number | string, description?: string): number { const registry = CollisionLayers.getRegistry(); // Handle overloaded method signature if (typeof indexOrDescription === 'number') { // First overload: registerCollisionLayer(name, index, description?) const index = indexOrDescription; const layer = registry.register(name, index, description); return layer.value; } else { // Second overload: registerCollisionLayer(name, description?) const layerDescription = indexOrDescription; const nextIndex = registry.getNextAvailableIndex(); if (nextIndex === -1) { throw new Error('No more collision layer indices available (maximum 16)'); } const layer = registry.register(name, nextIndex, layerDescription); return layer.value; } } /** * Gets a registered collision layer by name. * * @param name Name of the collision layer * @returns The numeric value of the registered collision layer or undefined if not found */ public getCollisionLayer(name: string): number | undefined { const registry = CollisionLayers.getRegistry(); const layer = registry.get(name); return layer?.value; } /** * Gets all registered collision layers. * * @returns Array of all registered collision layers */ public getCollisionLayers(): RegisteredCollisionLayer[] { const registry = CollisionLayers.getRegistry(); return registry.getAll(); } /** * Removes a registered collision layer. * * @param name Name of the collision layer to remove * @returns True if the layer was removed, false if it didn't exist */ public removeCollisionLayer(name: string): boolean { const registry = CollisionLayers.getRegistry(); return registry.remove(name); } /** * Clears all registered collision layers. */ public clearCollisionLayers(): void { const registry = CollisionLayers.getRegistry(); registry.clear(); } /** * Checks if two AABB rectangles overlap. * * @param a - The first AABB rectangle * @param b - The second AABB rectangle * @returns True if the rectangles overlap, false otherwise */ public aabbOverlap(a: AABBLike, b: AABBLike): boolean { return this.system.aabbOverlap(a, b); } }