315 lines
7.2 KiB
JavaScript
315 lines
7.2 KiB
JavaScript
import {Actions, compile, Context} from '@avocado/behavior';
|
|
import {compose, TickingPromise} from '@avocado/core';
|
|
import {StateProperty, Trait} from '@avocado/entity';
|
|
import {Rectangle, Vector} from '@avocado/math';
|
|
|
|
const decorate = compose(
|
|
StateProperty('isCheckingCollisions'),
|
|
StateProperty('isColliding'),
|
|
);
|
|
|
|
export default class Collider extends decorate(Trait) {
|
|
|
|
static behaviorTypes() {
|
|
return {
|
|
collidesWith: {
|
|
advanced: true,
|
|
type: 'bool',
|
|
label: 'I would collide with $1.',
|
|
args: [
|
|
['other', {
|
|
type: 'entity',
|
|
}],
|
|
],
|
|
},
|
|
doesNotCollideWith: {
|
|
advanced: true,
|
|
type: 'bool',
|
|
label: 'I would not collide with $1.',
|
|
args: [
|
|
['other', {
|
|
type: 'entity',
|
|
}],
|
|
],
|
|
},
|
|
setDoesCollideWith: {
|
|
advanced: true,
|
|
type: 'void',
|
|
label: 'Set $1 as colliding with myself.',
|
|
args: [
|
|
['other', {
|
|
type: 'entity',
|
|
}],
|
|
],
|
|
},
|
|
setDoesNotCollideWith: {
|
|
advanced: true,
|
|
type: 'void',
|
|
label: 'Set $1 as not colliding with myself.',
|
|
args: [
|
|
['other', {
|
|
type: 'entity',
|
|
}],
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
static defaultParams() {
|
|
return {
|
|
activeCollision: false,
|
|
collidesWithGroups: [
|
|
'default',
|
|
],
|
|
collisionEndActions: {
|
|
type: 'expressions',
|
|
expressions: [],
|
|
},
|
|
collisionStartActions: {
|
|
type: 'expressions',
|
|
expressions: [],
|
|
},
|
|
collisionGroup: 'default',
|
|
isSensor: false,
|
|
}
|
|
}
|
|
|
|
static defaultState() {
|
|
return {
|
|
isCheckingCollisions: true,
|
|
isColliding: true,
|
|
}
|
|
}
|
|
|
|
static describeParams() {
|
|
return {
|
|
activeCollision: {
|
|
type: 'bool',
|
|
label: 'Actively check collisions',
|
|
},
|
|
collidesWithGroups: {
|
|
type: 'object',
|
|
label: 'Collides with groups',
|
|
},
|
|
collisionEndActions: {
|
|
type: 'expressions',
|
|
label: 'Collision ending actions',
|
|
},
|
|
collisionStartActions: {
|
|
type: 'expressions',
|
|
label: 'Collision starting actions',
|
|
},
|
|
collisionGroup: {
|
|
type: 'string',
|
|
label: 'Collision group',
|
|
},
|
|
isSensor: {
|
|
type: 'bool',
|
|
label: 'Is sensor',
|
|
},
|
|
};
|
|
}
|
|
|
|
static describeState() {
|
|
return {
|
|
isCheckingCollisions: {
|
|
type: 'bool',
|
|
label: 'Is checking collisions',
|
|
},
|
|
isColliding: {
|
|
type: 'bool',
|
|
label: 'Is able to collide',
|
|
},
|
|
}
|
|
}
|
|
|
|
static type() {
|
|
return 'collider';
|
|
}
|
|
|
|
constructor(entity, params, state) {
|
|
super(entity, params, state);
|
|
const {
|
|
collidesWithGroups,
|
|
collisionEndActions,
|
|
collisionGroup,
|
|
collisionStartActions,
|
|
isSensor,
|
|
} = this.params;
|
|
this._collidesWithGroups = collidesWithGroups;
|
|
this._collisionEndActions = collisionEndActions.length > 0
|
|
? new Actions(compile(collisionEndActions))
|
|
: undefined;
|
|
this._collisionStartActions = collisionStartActions.length > 0
|
|
? new Actions(compile(collisionStartActions))
|
|
: undefined;
|
|
this._collisionGroup = collisionGroup;
|
|
this._doesNotCollideWith = [];
|
|
this._isCollidingWith = [];
|
|
this._isSensor = isSensor;
|
|
}
|
|
|
|
destroy() {
|
|
this.releaseAllCollisions();
|
|
}
|
|
|
|
checkActiveCollision() {
|
|
if (!this.params.activeCollision) {
|
|
return;
|
|
}
|
|
const layer = this.entity.layer;
|
|
if (!layer) {
|
|
return;
|
|
}
|
|
const query = Rectangle.compose(
|
|
Vector.sub(this.entity.position, [32, 32]),
|
|
[64, 64],
|
|
);
|
|
const thisAabb = Rectangle.translated(
|
|
this.entity.shape.aabb,
|
|
this.entity.position
|
|
);
|
|
const entities = layer.visibleEntities(query);
|
|
for (let i = 0; i < entities.length; ++i) {
|
|
const entity = entities[i];
|
|
if (entity === this.entity) {
|
|
continue;
|
|
}
|
|
if (!entity.is('collider') || !entity.is('shaped') || entity.is('physical')) {
|
|
continue;
|
|
}
|
|
const otherAabb = Rectangle.translated(
|
|
entity.shape.aabb,
|
|
entity.position
|
|
);
|
|
if (Rectangle.intersects(thisAabb, otherAabb)) {
|
|
if (!this.isCollidingWithEntity(entity)) {
|
|
this.entity.emit('collisionStart', entity);
|
|
entity.emit('collisionStart', this.entity);
|
|
}
|
|
}
|
|
else {
|
|
if (this.isCollidingWithEntity(entity)) {
|
|
this.entity.emit('collisionEnd', entity);
|
|
entity.emit('collisionEnd', this.entity);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
get collisionGroup() {
|
|
return this._collisionGroup;
|
|
}
|
|
|
|
get collidesWithGroups() {
|
|
return this._collidesWithGroups;
|
|
}
|
|
|
|
get isCollidingWith() {
|
|
return this._isCollidingWith;
|
|
}
|
|
|
|
isCollidingWithEntity(entity) {
|
|
return -1 !== this._isCollidingWith.indexOf(entity);
|
|
}
|
|
|
|
get isSensor() {
|
|
return this._isSensor;
|
|
}
|
|
|
|
pushCollisionTickingPromise(actions, other) {
|
|
const context = new Context({
|
|
entity: [this.entity, 'entity'],
|
|
other: [this.entity, 'entity'],
|
|
});
|
|
this.entity.addTickingPromise(actions.tickingPromise(context));
|
|
}
|
|
|
|
releaseAllCollisions() {
|
|
for (let i = 0; i < this._isCollidingWith.length; i++) {
|
|
const entity = this._isCollidingWith[i];
|
|
entity.emit('collisionEnd', this.entity);
|
|
}
|
|
this._isCollidingWith = [];
|
|
}
|
|
|
|
// eslint-disable-next-line class-methods-use-this
|
|
hooks() {
|
|
return {
|
|
|
|
contextTypeHints: () => [['other', 'entity']],
|
|
|
|
};
|
|
}
|
|
|
|
listeners() {
|
|
return {
|
|
|
|
collisionEnd: (other) => {
|
|
const index = this._isCollidingWith.indexOf(other);
|
|
if (-1 !== index) {
|
|
this._isCollidingWith.splice(index, 1);
|
|
if (this._collisionEndActions) {
|
|
this.pushCollisionTickingPromise(this._collisionEndActions, other);
|
|
}
|
|
}
|
|
},
|
|
|
|
collisionStart: (other) => {
|
|
const index = this._isCollidingWith.indexOf(other);
|
|
if (-1 === index) {
|
|
this._isCollidingWith.push(other);
|
|
if (this._collisionStartActions) {
|
|
this.pushCollisionTickingPromise(this._collisionStartActions, other);
|
|
}
|
|
}
|
|
},
|
|
|
|
removedFromRoom: () => {
|
|
this.releaseAllCollisions();
|
|
},
|
|
|
|
};
|
|
}
|
|
|
|
methods() {
|
|
return {
|
|
|
|
collidesWith: (entity) => {
|
|
if (!this.entity.isColliding || !entity.isColliding) {
|
|
return false;
|
|
}
|
|
if (-1 !== this._doesNotCollideWith.indexOf(entity)) {
|
|
return false;
|
|
}
|
|
const collisionGroup = entity.collisionGroup;
|
|
return -1 !== this._collidesWithGroups.indexOf(collisionGroup);
|
|
},
|
|
|
|
doesNotCollideWith: (entity) => {
|
|
return !this.entity.collidesWith(entity);
|
|
},
|
|
|
|
setDoesCollideWith: (entity) => {
|
|
const index = this._doesNotCollideWith.indexOf(entity);
|
|
if (-1 !== index) {
|
|
this._doesNotCollideWith.splice(index, 1);
|
|
}
|
|
},
|
|
|
|
setDoesNotCollideWith: (entity) => {
|
|
if (-1 === this._doesNotCollideWith.indexOf(entity)) {
|
|
this._doesNotCollideWith.push(entity);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
tick(elapsed) {
|
|
if (AVOCADO_SERVER) {
|
|
this.checkActiveCollision();
|
|
}
|
|
}
|
|
|
|
}
|