@dill-pixel/plugin-snap-physics
Version:
Snap Physics
267 lines (232 loc) • 8.68 kB
text/typescript
import { Application, filterSet } from 'dill-pixel';
import { gsap } from 'gsap';
import { Actor } from './Actor';
import { Entity } from './Entity';
import { System } from './System';
export class Solid<T = any, A extends Application = Application> extends Entity<T, A> {
type = 'Solid';
isSolid = true;
riding: Set<Actor> = new Set();
protected _animations: Set<gsap.core.Tween | gsap.core.Timeline> = new Set<gsap.core.Tween | gsap.core.Timeline>();
protected _positionAnimation: {
targetX: number;
targetY: number;
startX: number;
startY: number;
duration: number;
elapsed: number;
ease: gsap.EaseString;
repeat: number;
yoyo: boolean;
repeatDelay: number;
delayRemaining: number;
iteration: number;
isReversed: boolean;
} | null = null;
getCollideables<T extends Entity = Entity>(dx: number = 0, dy: number = 0): Set<T> {
return System.getNearbyEntities<T>(this, 'actor', dx, dy) as Set<T>;
}
added() {
System.addSolid(this);
this.addSignalConnection(this.system.onSystemEnabledChanged.connect(this._handleSystemEnabledChanged));
}
private _handleSystemEnabledChanged(enabled: boolean) {
if (enabled) {
if (this._animations?.size > 0) {
this._animations.forEach((animation) => animation?.resume());
}
} else {
if (this._animations?.size > 0) {
this._animations.forEach((animation) => animation?.pause());
}
}
}
removed() {
System.removeSolid(this);
if (this._animations?.size > 0) {
this._animations.forEach((animation) => animation?.kill());
}
}
getAllRiding(dx: number = 0, dy: number = 0) {
return filterSet<Actor>(
this.getCollideables(dx, dy),
(entity: Actor) => entity.isActor && entity.isRiding(this),
) as Set<Actor>;
}
move(x: number, y: number): void {
this.xRemainder += x;
this.yRemainder += y;
const moveX = Math.round(this.xRemainder);
const moveY = Math.round(this.yRemainder);
if (moveX !== 0 || moveY !== 0) {
// Get all potential collisions before moving
const ridingActors = this.getAllRiding(moveX, moveY);
// For moving platforms, we need to check a larger area to catch fast-moving actors
const sweepBox = this.boundingRect.clone();
// Expand the sweep box in the direction of movement
if (moveY < 0) {
// Moving up - expand upward
sweepBox.y += moveY;
sweepBox.height -= moveY;
} else {
// Moving down - expand downward
sweepBox.height += moveY;
}
if (moveX < 0) {
// Moving left - expand left
sweepBox.x += moveX;
sweepBox.width -= moveX;
} else {
// Moving right - expand right
sweepBox.width += moveX;
}
// Get potential collisions using the sweep box
const potentialCollisions = new Set<Actor>();
for (const actor of this.getCollideables<Actor>(moveX, moveY)) {
if (actor.boundingRect.intersects(sweepBox)) {
potentialCollisions.add(actor);
}
}
// First move riding actors with the platform
for (const actor of ridingActors) {
if (actor.mostRiding === this) {
// Move riding actors first to maintain contact
actor.moveY(moveY);
actor.moveX(moveX);
}
}
// Then move the platform
this.x += moveX;
this.y += moveY;
this.xRemainder -= moveX;
this.yRemainder -= moveY;
// Finally handle any collisions
this.handleActorInteractions(moveX, moveY, ridingActors, potentialCollisions);
}
System.updateEntity(this);
}
animatePosition(x?: number | null, y?: number | null, vars: gsap.TweenVars = {}): gsap.core.Tween {
const pos = this.position.clone();
const tweenVars = Object.assign({ duration: 1, ease: 'linear.none' }, vars);
const targetX = x === undefined || x === null ? pos.x : x;
const targetY = y === undefined || y === null ? pos.y : y;
// Store animation data for physics update
this._positionAnimation = {
targetX,
targetY,
startX: pos.x,
startY: pos.y,
duration: tweenVars.duration as number,
elapsed: 0,
ease: (tweenVars.ease?.toString() || 'linear.none') as gsap.EaseString,
repeat: (tweenVars.repeat as number) || 0,
yoyo: tweenVars.yoyo || false,
repeatDelay: (tweenVars.repeatDelay as number) || 0,
delayRemaining: 0,
iteration: 0,
isReversed: false,
};
// Create a dummy tween for compatibility
const tween = gsap.to({}, tweenVars);
this._animations.add(tween);
return tween;
}
fixedUpdate(deltaTime: number) {
super.fixedUpdate(deltaTime);
// Update position animation in sync with physics
if (this._positionAnimation) {
// Handle repeat delay
if (this._positionAnimation.delayRemaining > 0) {
this._positionAnimation.delayRemaining -= deltaTime;
return;
}
this._positionAnimation.elapsed += deltaTime;
const progress = Math.min(this._positionAnimation.elapsed / this._positionAnimation.duration, 1);
// Calculate the new position using GSAP's easing
const easedProgress = gsap.parseEase(this._positionAnimation.ease)(
this._positionAnimation.isReversed ? 1 - progress : progress,
);
const newX =
this._positionAnimation.startX +
(this._positionAnimation.targetX - this._positionAnimation.startX) * easedProgress;
const newY =
this._positionAnimation.startY +
(this._positionAnimation.targetY - this._positionAnimation.startY) * easedProgress;
// Apply movement through physics system
this.move(newX - this.x, newY - this.y);
// Handle completion of current iteration
if (progress >= 1) {
// Reset elapsed time
this._positionAnimation.elapsed = 0;
// Handle repeat logic
if (
this._positionAnimation.repeat === -1 ||
this._positionAnimation.iteration < this._positionAnimation.repeat
) {
this._positionAnimation.iteration++;
// Handle yoyo
if (this._positionAnimation.yoyo) {
this._positionAnimation.isReversed = !this._positionAnimation.isReversed;
}
// Apply repeat delay
if (this._positionAnimation.repeatDelay > 0) {
this._positionAnimation.delayRemaining = this._positionAnimation.repeatDelay;
}
} else {
// Animation complete
this._positionAnimation = null;
}
}
}
}
public handleActorInteractions(
deltaX: number,
deltaY: number,
ridingActors = this.getAllRiding(),
potentialCollisions = this.getCollideables<Actor>(deltaX, deltaY),
): void {
for (const actor of potentialCollisions) {
// Skip actors that are already riding (they were moved with the platform)
if (ridingActors.has(actor)) continue;
if (!actor.passThroughTypes.includes(this.type) && !actor.isPassingThrough(this)) {
// For moving platforms, we need to do a more thorough collision check
const isColliding = this.collidesWith(actor, deltaX, deltaY);
const wasColliding = this.collidesWith(actor, 0, 0);
// If either check detects a collision, handle it
if (isColliding || wasColliding) {
// Calculate overlaps
const overlapX =
deltaX !== 0
? deltaX > 0
? this.boundingRect.right - actor.boundingRect.left
: this.boundingRect.left - actor.boundingRect.right
: 0;
const overlapY =
deltaY !== 0
? deltaY > 0
? this.boundingRect.bottom - actor.boundingRect.top
: this.boundingRect.top - actor.boundingRect.bottom
: 0;
// For fast-moving platforms, prioritize vertical resolution
if (Math.abs(deltaY) > Math.abs(deltaX)) {
if (overlapY !== 0) {
actor.moveY(overlapY, actor.squish, null, this);
}
if (overlapX !== 0) {
actor.moveX(overlapX, actor.squish, null, this);
}
} else {
if (overlapX !== 0) {
actor.moveX(overlapX, actor.squish, null, this);
}
if (overlapY !== 0) {
actor.moveY(overlapY, actor.squish, null, this);
}
}
}
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected handleCollisionChange(_isColliding?: boolean) {}
}