+import { System } from '@serpentity/serpentity';
+import { Body, Constraint, SAT, World } from 'matter-js';
+
+import GrabberNode from '../nodes/grabber';
+import GrabbableNode from '../nodes/grabbable';
+
+import Config from '../config';
+
+const internals = {
+ kGrabRadius: 50,
+ kNoEngine: 'No matter js physics engine found. Make sure you set the `engine` key in the config object when initializing.'
+};
+
+/**
+ * Handles grabbing between entities
+ *
+ * @extends {external:Serpentity.System}
+ * @class GrabSystem
+ * @param {object} config a configuration object to extend.
+ */
+export default class GrabSystem extends System {
+
+ constructor(config = {}) {
+
+ super();
+
+ /**
+ * The node collection of grabbers
+ *
+ * @property {external:Serpentity.NodeCollection} grabbers
+ * @instance
+ * @memberof GrabSystem
+ */
+ this.grabbers = null;
+
+ /**
+ * The node collection of grabbables
+ *
+ * @property {external:Serpentity.NodeCollection} grabbables
+ * @instance
+ * @memberof GrabSystem
+ */
+ this.grabbables = null;
+
+ /**
+ * The matter-js engine we will use to add and remove constraints
+ *
+ * @property {external:MatterJs.Engine} engine
+ * @instance
+ * @memberof GrabSystem
+ */
+ this.engine = config.engine;
+
+ if (!this.engine) {
+ throw new Error(internals.kNoEngine);
+ }
+ }
+
+ /**
+ * Initializes system when added. Requests grabber and grabbable nodes
+ *
+ * @function added
+ * @memberof GrabSystem
+ * @instance
+ * @param {external:Serpentity.Engine} engine the serpentity engine to
+ * which we are getting added
+ */
+ added(engine) {
+
+ this.grabbers = engine.getNodes(GrabberNode);
+ this.grabbables = engine.getNodes(GrabbableNode);
+ }
+
+ /**
+ * Clears system resources when removed.
+ *
+ * @function removed
+ * @instance
+ * @memberof GrabSystem
+ */
+ removed() {
+
+ this.grabbers = null;
+ this.grabbables = null;
+ }
+
+ /**
+ * Runs on every update of the loop. Triggers grab and manages cooldown
+ *
+ * @function update
+ * @instance
+ * @param {Number} currentFrameDuration the duration of the current
+ * frame
+ * @memberof GrabSystem
+ */
+ update(currentFrameDuration) {
+
+ for (const grabber of this.grabbers) {
+
+ const grab = grabber.grab;
+
+ if (grab.grabbing && !grab.locked) {
+ this._grab(grabber);
+ }
+
+ const isGrabReleased = !grab.grabbing || grab.currentCooldown >= grab.cooldown;
+ if (grab.constraint && isGrabReleased) {
+ this._release(grabber);
+ }
+
+ if (!grab.grabbing && grab.locked && grab.currentCooldown >= grab.cooldown) {
+ this._unlock(grabber);
+ }
+
+ if (grab.locked) {
+ grab.currentCooldown += currentFrameDuration;
+ }
+
+ grab.grabbing = 0;
+ }
+ }
+
+ // Executes the dash action
+
+ _grab(grabber) {
+
+ const grab = grabber.grab;
+
+ grab.locked = true;
+ grab.currentCooldown = 0;
+
+ Body.setPosition(grabber.grabArea.area, grabber.body.body.position);
+
+ console.log('Grab!');
+
+ for (const grabbable of this.grabbables) {
+
+ if (grabbable.entity === grabber.entity) {
+ continue;
+ }
+
+ const collision = SAT.collides(grabber.grabArea.area, grabbable.body.body);
+ if (collision.collided) {
+ grab.constraint = this._createConstraint(grabber.body.body, grabbable.body.body);
+ console.log('Grabbing', grab.constraint);
+ }
+ }
+ }
+
+ // Executes the unlock action
+
+ _unlock(grabber) {
+
+ grabber.grab.locked = false;
+ }
+
+ // Releases a constraint
+
+ _release(grabber) {
+
+ console.log('Releasing', grabber.grab.constraint);
+ World.remove(this.engine.world, grabber.grab.constraint);
+ grabber.grab.currentCooldown = 0;
+ grabber.grab.constraint = null;
+ }
+
+ // Performs a grab between two entities
+
+ _createConstraint(grabber, grabbable) {
+
+ const constraint = Constraint.create({ // Attach the sensor to the body
+ bodyA: grabber,
+ bodyB: grabbable,
+ damping: 0,
+ length: internals.kGrabRadius / Config.meterSize,
+ stiffness: 1
+ });
+
+ World.add(this.engine.world, [constraint]);
+
+ return constraint;
+ }
+};