const BlockType = require('../../extension-support/block-type') const BlockShape = require('../../extension-support/block-shape') const ArgumentType = require('../../extension-support/argument-type') const TargetType = require('../../extension-support/target-type') const Cast = require('../../util/cast') const Matter = require('matter-js') let Vector = { Type: class {}, Block: {}, Argument: {} } let jwArray = { Type: class {}, Block: {}, Argument: {} } let Target = { Type: class {}, Block: {}, Argument: {} } class Extension { constructor() { if (!vm.jwVector) vm.extensionManager.loadExtensionIdSync('jwVector') Vector = vm.jwVector if (!vm.jwArray) vm.extensionManager.loadExtensionIdSync('jwArray') jwArray = vm.jwArray if (!vm.jwTargets) vm.extensionManager.loadExtensionIdSync('jwTargets') Target = vm.jwTargets this.engine = Matter.Engine.create() /** @type {Object} */ this.bodies = {} /** @type {Matter.Composite?} */ this.bounds = null vm.runtime.on("PROJECT_START", this.reset.bind(this)); vm.PsychicDebug = this; } getInfo() { return { id: "jwPsychic", name: "Psychic", color1: "#b16bed", menuIconURI: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCIgeG1sbnM6Yng9Imh0dHBzOi8vYm94eS1zdmcuY29tIiB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4Ij48ZGVmcz48Yng6ZXhwb3J0PjxieDpmaWxlIGZvcm1hdD0ic3ZnIi8+PC9ieDpleHBvcnQ+PC9kZWZzPjxlbGxpcHNlIHN0eWxlPSJzdHJva2Utd2lkdGg6IDJweDsgcGFpbnQtb3JkZXI6IHN0cm9rZTsgZmlsbDogcmdiKDE3NywgMTA3LCAyMzcpOyBzdHJva2U6IHJnYigxNTksIDk2LCAyMTMpOyIgY3g9IjEwIiBjeT0iMTAiIHJ4PSI5IiByeT0iOSIvPjxyZWN0IHg9IjQuNjM0IiB5PSIxMC4yMjgiIHdpZHRoPSI0Ljc3IiBoZWlnaHQ9IjQuNzciIHN0eWxlPSJmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7Ii8+PHJlY3QgeD0iMTAuNTk2IiB5PSIxMC4yMjgiIHdpZHRoPSI0Ljc3IiBoZWlnaHQ9IjQuNzciIHN0eWxlPSJmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7Ii8+PHJlY3QgeD0iNy42MTUiIHdpZHRoPSI0Ljc3IiBoZWlnaHQ9IjQuNzciIHN0eWxlPSJmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7IiB5PSI0LjI2NyIvPjwvc3ZnPg==", blocks: [ { opcode: 'tick', text: 'tick', blockType: BlockType.COMMAND }, "---", { opcode: 'boundaries', text: 'set boundaries [OPTION]', blockType: BlockType.COMMAND, arguments: { OPTION: { type: ArgumentType.STRING, menu: 'boundariesOption' } } }, { opcode: 'setGravity', text: 'set gravity to [VECTOR]', blockType: BlockType.COMMAND, arguments: { VECTOR: Vector.Argument } }, { opcode: 'getGravity', text: 'gravity', ...Vector.Block }, "---", { opcode: 'enablePhysics', text: 'enable physics as [OPTION]', blockType: BlockType.COMMAND, arguments: { OPTION: { type: ArgumentType.STRING, menu: 'enablePhysicsOption' } }, filter: [TargetType.SPRITE] }, { opcode: 'disablePhysics', text: 'disable physics', blockType: BlockType.COMMAND, filter: [TargetType.SPRITE] }, "---", { opcode: 'setPos', text: 'set position to [VECTOR]', blockType: BlockType.COMMAND, arguments: { VECTOR: Vector.Argument }, filter: [TargetType.SPRITE] }, { opcode: 'getPos', text: 'position', filter: [TargetType.SPRITE], ...Vector.Block }, { opcode: 'setVel', text: 'set velocity to [VECTOR]', blockType: BlockType.COMMAND, arguments: { VECTOR: Vector.Argument }, filter: [TargetType.SPRITE] }, { opcode: 'getVel', text: 'velocity', filter: [TargetType.SPRITE], ...Vector.Block }, { opcode: 'setRot', text: 'set rotation to [ANGLE]', blockType: BlockType.COMMAND, arguments: { ANGLE: { type: ArgumentType.ANGLE, defaultValue: 90 } }, filter: [TargetType.SPRITE] }, { opcode: 'getRot', text: 'rotation', blockType: BlockType.REPORTER, filter: [TargetType.SPRITE] }, { opcode: 'setAngVel', text: 'set angular velocity to [ANGLE]', blockType: BlockType.COMMAND, arguments: { ANGLE: { type: ArgumentType.ANGLE, defaultValue: 0 } }, filter: [TargetType.SPRITE] }, { opcode: 'getAngVel', text: 'angular velocity', blockType: BlockType.REPORTER, filter: [TargetType.SPRITE] }, "---", { opcode: 'getMass', text: 'mass', blockType: BlockType.REPORTER, filter: [TargetType.SPRITE] }, { opcode: 'setDensity', text: 'set density to [NUMBER]', blockType: BlockType.COMMAND, arguments: { NUMBER: { type: ArgumentType.NUMBER, defaultValue: 0.001 } }, filter: [TargetType.SPRITE] }, { opcode: 'getDensity', text: 'density', blockType: BlockType.REPORTER, filter: [TargetType.SPRITE] }, "---", { opcode: 'setStatic', text: 'set fixed to [BOOLEAN]', blockType: BlockType.COMMAND, arguments: { BOOLEAN: { type: ArgumentType.BOOLEAN } }, filter: [TargetType.SPRITE] }, { opcode: 'getStatic', text: 'fixed', blockType: BlockType.BOOLEAN, filter: [TargetType.SPRITE] }, { opcode: 'setRotatable', text: 'set rotatable to [BOOLEAN]', blockType: BlockType.COMMAND, arguments: { BOOLEAN: { type: ArgumentType.BOOLEAN } }, filter: [TargetType.SPRITE] }, { opcode: 'getRotatable', text: 'rotatable', blockType: BlockType.BOOLEAN, filter: [TargetType.SPRITE] }, "---", { opcode: 'setFric', text: 'set friction to [NUMBER]', blockType: BlockType.COMMAND, arguments: { NUMBER: { type: ArgumentType.NUMBER, defaultValue: 0.1 } }, filter: [TargetType.SPRITE] }, { opcode: 'getFric', text: 'friction', blockType: BlockType.REPORTER, filter: [TargetType.SPRITE] }, { opcode: 'setAirFric', text: 'set air resistance to [NUMBER]', blockType: BlockType.COMMAND, arguments: { NUMBER: { type: ArgumentType.NUMBER, defaultValue: 0.01 } }, filter: [TargetType.SPRITE] }, { opcode: 'getAirFric', text: 'air resistance', blockType: BlockType.REPORTER, filter: [TargetType.SPRITE] }, { opcode: 'setRest', text: 'set restitution to [NUMBER]', blockType: BlockType.COMMAND, arguments: { NUMBER: { type: ArgumentType.NUMBER, defaultValue: 0 } }, filter: [TargetType.SPRITE] }, { opcode: 'getRest', text: 'restitution', blockType: BlockType.REPORTER, filter: [TargetType.SPRITE] }, "---", { opcode: 'getCollides', text: 'targets colliding with [OPTION]', arguments: { OPTION: { type: ArgumentType.STRING, menu: 'touchingOption' } }, filter: [TargetType.SPRITE], ...jwArray.Block } ], menus: { enablePhysicsOption: [ 'precise', 'box', 'circle' ], boundariesOption: [ 'all', 'floor', 'none' ], touchingOption: [ 'body', 'feet', 'head' ] } }; } vectorToMatter(vector) { return Matter.Vector.create(vector.x, -vector.y) } matterToVector(matter) { return new Vector.Type(matter.x, -matter.y) } angleToMatter(angle) { return (angle - 90) * Math.PI / 180 } matterToAngle(matter) { return (matter * 180 / Math.PI) + 90 } reset() { this.engine = Matter.Engine.create() this.bodies = {} this.bounds = null } correctBody(id) { /** @type {Matter.Body} */ let body = this.bodies[id] let target = vm.runtime.getTargetById(id) if (target == undefined) { Matter.Composite.remove(this.engine.world, body) delete this.bodies[id] return } Matter.Body.setPosition(body, Matter.Vector.create(target.x, -target.y)) Matter.Body.setAngle(body, this.angleToMatter(target.direction)) } correctTarget(id) { /** @type {Matter.Body} */ let body = this.bodies[id] let target = vm.runtime.getTargetById(id) target.setXY(body.position.x, -body.position.y, false, true) target.setDirection(this.matterToAngle(body.angle)) } tick() { let fps = vm.runtime.frameLoop.framerate if (fps == 0) fps = 60 for (let id of Object.keys(this.bodies)) { this.correctBody(id) } Matter.Engine.update(this.engine, 1000 / fps) for (let id of Object.keys(this.bodies)) { this.correctTarget(id) } } boundaries({OPTION}) { if (this.bounds) { Matter.Composite.remove(this.engine.world, this.bounds) this.bounds = null } let stageWidth = vm.runtime.stageWidth let stageHeight = vm.runtime.stageHeight this.bounds = Matter.Composite.create() switch (OPTION) { case 'all': Matter.Composite.add(this.bounds, [ Matter.Bodies.rectangle(-stageWidth, 0, stageWidth, Number.MAX_SAFE_INTEGER / 2, { isStatic: true }), Matter.Bodies.rectangle(stageWidth, 0, stageWidth, Number.MAX_SAFE_INTEGER / 2, { isStatic: true }), Matter.Bodies.rectangle(0, -stageHeight, Number.MAX_SAFE_INTEGER / 2, stageHeight, { isStatic: true }), ]) case 'floor': Matter.Composite.add(this.bounds, Matter.Bodies.rectangle(0, stageHeight, Number.MAX_SAFE_INTEGER / 2, stageHeight, { isStatic: true })) break } Matter.Composite.add(this.engine.world, this.bounds) } setGravity({VECTOR}) { let v = Vector.Type.toVector(VECTOR) this.engine.gravity.x = v.x this.engine.gravity.y = -v.y } getGravity() { return this.matterToVector(this.engine.gravity) } enablePhysics({OPTION}, util) { let target = util.target let costume = target.getCostumes()[target.currentCostume] let size = { x: costume.size[0] * (target.size / 100) * (target.stretch[0] / 100) / costume.bitmapResolution, y: costume.size[1] * (target.size / 100) * (target.stretch[1] / 100) / costume.bitmapResolution } console.debug(size) /** @type {Matter.Body?} */ let body = null switch (OPTION) { case 'precise': throw "i need to finish precise mb" break case 'box': body = Matter.Bodies.rectangle(target.x, -target.y, size.x, size.y) break case 'circle': body = Matter.Bodies.circle(target.x, -target.y, Math.max(size.x, size.y) / 2) break default: throw "Invalid physics option" } body.label = target.id this.bodies[target.id] = body Matter.Composite.add(this.engine.world, body) this.correctBody(target.id) } disablePhysics({}, util) { let body = this.bodies[util.target.id] if (!body) return Matter.Composite.remove(this.engine.world, body) delete this.bodies[id] return } setPos({VECTOR}, util) { let v = Vector.Type.toVector(VECTOR) util.target.setXY(v.x, v.y) } getPos({}, util) { let body = this.bodies[util.target.id] if (!body) return new Vector.Type(util.target.x, util.target.y) return this.matterToVector(body.position) } setRot({ANGLE}, util) { let a = Cast.toNumber(ANGLE) util.target.setDirection(a) } getRot({}, util) { let body = this.bodies[util.target.id] if (!body) return util.target.direction return this.matterToAngle(body.angle) } setVel({VECTOR}, util) { let body = this.bodies[util.target.id] if (!body) return let v = Vector.Type.toVector(VECTOR) Matter.Body.setVelocity(body, this.vectorToMatter(v)) } getVel({}, util) { let body = this.bodies[util.target.id] if (!body) return new Vector.Type(0, 0) return this.matterToVector(body.velocity) } setAngVel({ANGLE}, util) { let body = this.bodies[util.target.id] if (!body) return Matter.Body.setAngularVelocity(body, Cast.toNumber(ANGLE)) } getAngVel({}, util) { let body = this.bodies[util.target.id] if (!body) return 0 return body.angularVelocity } getMass({}, util) { let body = this.bodies[util.target.id] if (!body) return 0 return body.mass } getDensity({}, util) { let body = this.bodies[util.target.id] if (!body) return 0.001 return body.density } setDensity({NUMBER}, util) { let body = this.bodies[util.target.id] if (!body) return Matter.Body.setDensity(Cast.toNumber(NUMBER)) } getStatic({}, util) { let body = this.bodies[util.target.id] if (!body) return false return body.isStatic } setStatic({BOOLEAN}, util) { let body = this.bodies[util.target.id] if (!body) return body.isStatic = BOOLEAN } getRotatable({}, util) { let body = this.bodies[util.target.id] if (!body) return true return body.inertia !== Infinity } setRotatable({BOOLEAN}, util) { let body = this.bodies[util.target.id] if (!body) return if (BOOLEAN) { Matter.Body.setVertices(body, body.vertices) } else { Matter.Body.setInertia(body, Infinity) } } setFric({NUMBER}, util) { let body = this.bodies[util.target.id] if (!body) return body.friction = Cast.toNumber(NUMBER) } getFric({}, util) { let body = this.bodies[util.target.id] if (!body) return 0.1 return body.friction } setAirFric({NUMBER}, util) { let body = this.bodies[util.target.id] if (!body) return body.frictionAir = Cast.toNumber(NUMBER) } getAirFric({}, util) { let body = this.bodies[util.target.id] if (!body) return 0.01 return body.frictionAir } setRest({NUMBER}, util) { let body = this.bodies[util.target.id] if (!body) return body.restitution = Cast.toNumber(NUMBER) } getRest({}, util) { let body = this.bodies[util.target.id] if (!body) return 0.01 return body.restitution } getCollides({OPTION}, util) { let body = this.bodies[util.target.id] if (!body) return new jwArray.Type() let collisions = Matter.Query.collides(body, Object.values(this.bodies).filter(v => v.label !== util.target.id)) if (OPTION !== 'body') { collisions = collisions.filter(v => v.supports[0].x > body.bounds.min.x+1 && v.supports[0].x < body.bounds.max.x-1) console.debug(collisions) switch (OPTION) { case 'feet': collisions = collisions.filter(v => { for (let support of v.supports) { if (support == null) continue if (support.y > body.bounds.max.y-4) return true } }) break case 'head': collisions = collisions.filter(v => { for (let support of v.supports) { if (support == null) continue if (support.y < body.bounds.min.y+4) return true } }) break } console.debug(collisions) } let bodies = collisions.map(v => body == v.bodyA ? v.bodyB : v.bodyA) bodies.filter(v => v.label !== util.target.id) return new jwArray.Type(bodies.map(v => new Target.Type(v.label))) } } module.exports = Extension