Spaces:
Runtime error
Runtime error
| const Cast = require('../util/cast'); | |
| const MathUtil = require('../util/math-util'); | |
| const Timer = require('../util/timer'); | |
| const STAGE_ALIGNMENT = { | |
| TOP_LEFT: 'top-left', | |
| TOP_RIGHT: 'top-right', | |
| BOTTOM_LEFT: 'bottom-left', | |
| BOTTOM_RIGHT: 'bottom-right', | |
| TOP: 'top', | |
| LEFT: 'left', | |
| RIGHT: 'right', | |
| MIDDLE: 'middle', | |
| BOTTOM: 'bottom' | |
| }; | |
| class Scratch3MotionBlocks { | |
| constructor (runtime) { | |
| /** | |
| * The runtime instantiating this block package. | |
| * @type {Runtime} | |
| */ | |
| this.runtime = runtime; | |
| } | |
| /** | |
| * Retrieve the block primitives implemented by this package. | |
| * @return {object.<string, Function>} Mapping of opcode to Function. | |
| */ | |
| getPrimitives () { | |
| return { | |
| motion_movesteps: this.moveSteps, | |
| motion_movebacksteps: this.moveStepsBack, | |
| motion_moveupdownsteps: this.moveStepsUpDown, | |
| motion_gotoxy: this.goToXY, | |
| motion_goto: this.goTo, | |
| motion_turnright: this.turnRight, | |
| motion_turnleft: this.turnLeft, | |
| motion_turnrightaroundxy: this.turnRightAround, | |
| motion_turnleftaroundxy: this.turnLeftAround, | |
| motion_turnaround: this.turnAround, | |
| motion_pointinrandomdirection: this.pointInDirectionRandom, | |
| motion_pointtowardsxy: this.pointTowardsXY, | |
| motion_pointindirection: this.pointInDirection, | |
| motion_pointtowards: this.pointTowards, | |
| motion_glidesecstoxy: this.glide, | |
| motion_glideto: this.glideTo, | |
| motion_ifonedgebounce: this.ifOnEdgeBounce, | |
| motion_ifonxybounce: this.ifOnXYBounce, | |
| motion_ifonspritebounce: this.ifOnSpriteBounce, | |
| motion_setrotationstyle: this.setRotationStyle, | |
| motion_changexby: this.changeX, | |
| motion_setx: this.setX, | |
| motion_changeyby: this.changeY, | |
| motion_sety: this.setY, | |
| motion_changebyxy: this.changeXY, | |
| motion_xposition: this.getX, | |
| motion_yposition: this.getY, | |
| motion_direction: this.getDirection, | |
| motion_move_sprite_to_scene_side: this.moveToStageSide, | |
| // Legacy no-op blocks: | |
| motion_scroll_right: () => {}, | |
| motion_scroll_up: () => {}, | |
| motion_align_scene: () => {}, | |
| motion_xscroll: () => {}, | |
| motion_yscroll: () => {} | |
| }; | |
| } | |
| moveToStageSide(args, util) { | |
| if (!this.runtime.renderer) return; | |
| const side = Cast.toString(args.ALIGNMENT); | |
| const stageWidth = this.runtime.stageWidth / 2; | |
| const stageHeight = this.runtime.stageHeight / 2; | |
| const snap = []; | |
| switch (side) { | |
| case STAGE_ALIGNMENT.TOP: | |
| util.target.setXY(0, stageHeight); | |
| snap.push('top'); | |
| break; | |
| case STAGE_ALIGNMENT.LEFT: | |
| util.target.setXY(0 - stageWidth, 0); | |
| snap.push('left'); | |
| break; | |
| case STAGE_ALIGNMENT.MIDDLE: | |
| util.target.setXY(0, 0); | |
| break; | |
| case STAGE_ALIGNMENT.RIGHT: | |
| util.target.setXY(stageWidth, 0); | |
| snap.push('right'); | |
| break; | |
| case STAGE_ALIGNMENT.BOTTOM: | |
| util.target.setXY(0, 0 - stageHeight); | |
| snap.push('bottom'); | |
| break; | |
| case STAGE_ALIGNMENT.TOP_LEFT: | |
| util.target.setXY(0 - stageWidth, stageHeight); | |
| snap.push('top'); | |
| snap.push('left'); | |
| break; | |
| case STAGE_ALIGNMENT.TOP_RIGHT: | |
| util.target.setXY(stageWidth, stageHeight); | |
| snap.push('top'); | |
| snap.push('right'); | |
| break; | |
| case STAGE_ALIGNMENT.BOTTOM_LEFT: | |
| util.target.setXY(0 - stageWidth, 0 - stageHeight); | |
| snap.push('bottom'); | |
| snap.push('left'); | |
| break; | |
| case STAGE_ALIGNMENT.BOTTOM_RIGHT: | |
| util.target.setXY(stageWidth, 0 - stageHeight); | |
| snap.push('bottom'); | |
| snap.push('right'); | |
| break; | |
| } | |
| const drawableID = util.target.drawableID; | |
| const drawable = this.runtime.renderer._allDrawables[drawableID]; | |
| const boundingBox = drawable._skin.getFenceBounds(drawable); | |
| snap.forEach(side => { | |
| switch (side) { | |
| case 'top': | |
| util.target.setXY(util.target.x, boundingBox.bottom); | |
| break; | |
| case 'bottom': | |
| util.target.setXY(util.target.x, boundingBox.top); | |
| break; | |
| case 'left': | |
| util.target.setXY(boundingBox.right, util.target.y); | |
| break; | |
| case 'right': | |
| util.target.setXY(boundingBox.left, util.target.y); | |
| break; | |
| } | |
| }); | |
| } | |
| getMonitored () { | |
| return { | |
| motion_xposition: { | |
| isSpriteSpecific: true, | |
| getId: targetId => `${targetId}_xposition` | |
| }, | |
| motion_yposition: { | |
| isSpriteSpecific: true, | |
| getId: targetId => `${targetId}_yposition` | |
| }, | |
| motion_direction: { | |
| isSpriteSpecific: true, | |
| getId: targetId => `${targetId}_direction` | |
| } | |
| }; | |
| } | |
| moveSteps (args, util) { | |
| const steps = Cast.toNumber(args.STEPS); | |
| this._moveSteps(steps, util.target); | |
| } | |
| moveStepsBack (args, util) { | |
| const steps = Cast.toNumber(args.STEPS); | |
| this._moveSteps(0 - steps, util.target); | |
| } | |
| moveStepsUpDown (args, util) { | |
| const direction = Cast.toString(args.DIRECTION); | |
| const steps = Cast.toNumber(args.STEPS); | |
| this.turnLeft({ DEGREES: 90 }, util); | |
| if (direction === 'up') { | |
| this._moveSteps(steps, util.target); | |
| } else if (direction === 'down') { | |
| this._moveSteps(0 - steps, util.target); | |
| } | |
| this.turnRight({ DEGREES: 90 }, util); | |
| } | |
| _moveSteps (steps, target) { // used by compiler | |
| const radians = MathUtil.degToRad(90 - target.direction); | |
| const dx = steps * Math.cos(radians); | |
| const dy = steps * Math.sin(radians); | |
| target.setXY(target.x + dx, target.y + dy); | |
| } | |
| goToXY (args, util) { | |
| const x = Cast.toNumber(args.X); | |
| const y = Cast.toNumber(args.Y); | |
| util.target.setXY(x, y); | |
| } | |
| getTargetXY (targetName, util) { | |
| let targetX = 0; | |
| let targetY = 0; | |
| if (targetName === '_mouse_') { | |
| targetX = util.ioQuery('mouse', 'getScratchX'); | |
| targetY = util.ioQuery('mouse', 'getScratchY'); | |
| } else if (targetName === '_random_') { | |
| const stageWidth = this.runtime.stageWidth; | |
| const stageHeight = this.runtime.stageHeight; | |
| targetX = Math.round(stageWidth * (Math.random() - 0.5)); | |
| targetY = Math.round(stageHeight * (Math.random() - 0.5)); | |
| } else { | |
| targetName = Cast.toString(targetName); | |
| const goToTarget = this.runtime.getSpriteTargetByName(targetName); | |
| if (!goToTarget) return; | |
| targetX = goToTarget.x; | |
| targetY = goToTarget.y; | |
| } | |
| return [targetX, targetY]; | |
| } | |
| goTo (args, util) { | |
| const targetXY = this.getTargetXY(args.TO, util); | |
| if (targetXY) { | |
| util.target.setXY(targetXY[0], targetXY[1]); | |
| } | |
| } | |
| turnRight (args, util) { | |
| const degrees = Cast.toNumber(args.DEGREES); | |
| util.target.setDirection(util.target.direction + degrees); | |
| } | |
| turnLeft (args, util) { | |
| const degrees = Cast.toNumber(args.DEGREES); | |
| util.target.setDirection(util.target.direction - degrees); | |
| } | |
| turnRightAround (args, util) { | |
| this.turnLeftAround({ | |
| DEGREES: -Cast.toNumber(args.DEGREES), | |
| X: Cast.toNumber(args.X), | |
| Y: Cast.toNumber(args.Y) | |
| }, util); | |
| } | |
| turnLeftAround (args, util) { | |
| const degrees = Cast.toNumber(args.DEGREES); | |
| const center = { | |
| x: Cast.toNumber(args.X), | |
| y: Cast.toNumber(args.Y) | |
| }; | |
| const radians = (Math.PI * degrees) / 180; | |
| const cos = Math.cos(radians); | |
| const sin = Math.sin(radians); | |
| const dx = util.target.x - center.x; | |
| const dy = util.target.y - center.y; | |
| const newPosition = { | |
| x: (cos * dx) - (sin * dy) + center.x, | |
| y: (cos * dy) + (sin * dx) + center.y | |
| }; | |
| util.target.setXY(newPosition.x, newPosition.y); | |
| } | |
| pointInDirection (args, util) { | |
| const direction = Cast.toNumber(args.DIRECTION); | |
| util.target.setDirection(direction); | |
| } | |
| turnAround (_, util) { | |
| this.turnRight({ DEGREES: 180 }, util); | |
| } | |
| pointInDirectionRandom (_, util) { | |
| this.pointTowards({ TOWARDS: '_random_' }, util); | |
| } | |
| pointTowardsXY (args, util) { | |
| const targetX = Cast.toNumber(args.X); | |
| const targetY = Cast.toNumber(args.Y); | |
| const dx = targetX - util.target.x; | |
| const dy = targetY - util.target.y; | |
| const direction = 90 - MathUtil.radToDeg(Math.atan2(dy, dx)); | |
| util.target.setDirection(direction); | |
| } | |
| pointTowards (args, util) { | |
| let targetX = 0; | |
| let targetY = 0; | |
| if (args.TOWARDS === '_mouse_') { | |
| targetX = util.ioQuery('mouse', 'getScratchX'); | |
| targetY = util.ioQuery('mouse', 'getScratchY'); | |
| } else if (args.TOWARDS === '_random_') { | |
| util.target.setDirection(Math.round(Math.random() * 360) - 180); | |
| return; | |
| } else { | |
| args.TOWARDS = Cast.toString(args.TOWARDS); | |
| const pointTarget = this.runtime.getSpriteTargetByName(args.TOWARDS); | |
| if (!pointTarget) return; | |
| targetX = pointTarget.x; | |
| targetY = pointTarget.y; | |
| } | |
| const dx = targetX - util.target.x; | |
| const dy = targetY - util.target.y; | |
| const direction = 90 - MathUtil.radToDeg(Math.atan2(dy, dx)); | |
| util.target.setDirection(direction); | |
| } | |
| glide (args, util) { | |
| if (util.stackFrame.timer) { | |
| const timeElapsed = util.stackFrame.timer.timeElapsed(); | |
| if (timeElapsed < util.stackFrame.duration * 1000) { | |
| // In progress: move to intermediate position. | |
| const frac = timeElapsed / (util.stackFrame.duration * 1000); | |
| const dx = frac * (util.stackFrame.endX - util.stackFrame.startX); | |
| const dy = frac * (util.stackFrame.endY - util.stackFrame.startY); | |
| util.target.setXY( | |
| util.stackFrame.startX + dx, | |
| util.stackFrame.startY + dy | |
| ); | |
| util.yield(); | |
| } else { | |
| // Finished: move to final position. | |
| util.target.setXY(util.stackFrame.endX, util.stackFrame.endY); | |
| } | |
| } else { | |
| // First time: save data for future use. | |
| util.stackFrame.timer = new Timer(); | |
| util.stackFrame.timer.start(); | |
| util.stackFrame.duration = Cast.toNumber(args.SECS); | |
| util.stackFrame.startX = util.target.x; | |
| util.stackFrame.startY = util.target.y; | |
| util.stackFrame.endX = Cast.toNumber(args.X); | |
| util.stackFrame.endY = Cast.toNumber(args.Y); | |
| if (util.stackFrame.duration <= 0) { | |
| // Duration too short to glide. | |
| util.target.setXY(util.stackFrame.endX, util.stackFrame.endY); | |
| return; | |
| } | |
| util.yield(); | |
| } | |
| } | |
| glideTo (args, util) { | |
| const targetXY = this.getTargetXY(args.TO, util); | |
| if (targetXY) { | |
| this.glide({SECS: args.SECS, X: targetXY[0], Y: targetXY[1]}, util); | |
| } | |
| } | |
| ifOnEdgeBounce (args, util) { | |
| this._ifOnEdgeBounce(util.target); | |
| } | |
| _ifOnEdgeBounce (target) { // used by compiler | |
| const bounds = target.getBounds(); | |
| if (!bounds) { | |
| return; | |
| } | |
| // Measure distance to edges. | |
| // Values are positive when the sprite is far away, | |
| // and clamped to zero when the sprite is beyond. | |
| const stageWidth = this.runtime.stageWidth; | |
| const stageHeight = this.runtime.stageHeight; | |
| const distLeft = Math.max(0, (stageWidth / 2) + bounds.left); | |
| const distTop = Math.max(0, (stageHeight / 2) - bounds.top); | |
| const distRight = Math.max(0, (stageWidth / 2) - bounds.right); | |
| const distBottom = Math.max(0, (stageHeight / 2) + bounds.bottom); | |
| // Find the nearest edge. | |
| let nearestEdge = ''; | |
| let minDist = Infinity; | |
| if (distLeft < minDist) { | |
| minDist = distLeft; | |
| nearestEdge = 'left'; | |
| } | |
| if (distTop < minDist) { | |
| minDist = distTop; | |
| nearestEdge = 'top'; | |
| } | |
| if (distRight < minDist) { | |
| minDist = distRight; | |
| nearestEdge = 'right'; | |
| } | |
| if (distBottom < minDist) { | |
| minDist = distBottom; | |
| nearestEdge = 'bottom'; | |
| } | |
| if (minDist > 0) { | |
| return; // Not touching any edge. | |
| } | |
| // Point away from the nearest edge. | |
| const radians = MathUtil.degToRad(90 - target.direction); | |
| let dx = Math.cos(radians); | |
| let dy = -Math.sin(radians); | |
| if (nearestEdge === 'left') { | |
| dx = Math.max(0.2, Math.abs(dx)); | |
| } else if (nearestEdge === 'top') { | |
| dy = Math.max(0.2, Math.abs(dy)); | |
| } else if (nearestEdge === 'right') { | |
| dx = 0 - Math.max(0.2, Math.abs(dx)); | |
| } else if (nearestEdge === 'bottom') { | |
| dy = 0 - Math.max(0.2, Math.abs(dy)); | |
| } | |
| const newDirection = MathUtil.radToDeg(Math.atan2(dy, dx)) + 90; | |
| target.setDirection(newDirection); | |
| // Keep within the stage. | |
| const fencedPosition = target.keepInFence(target.x, target.y); | |
| target.setXY(fencedPosition[0], fencedPosition[1]); | |
| } | |
| ifOnXYBounce(args, util, _, __, ___, touchingCondition) { | |
| const x = Cast.toNumber(args.X); | |
| const y = Cast.toNumber(args.Y); | |
| const target = util.target; | |
| const bounds = target.getBounds(); | |
| if (!bounds) { | |
| return; | |
| } | |
| // Check to see if the point is inside the bounding box. | |
| const xInBounds = (x >= bounds.left) && (x <= bounds.right); | |
| const yInBounds = (y >= bounds.bottom) && (y <= bounds.top); | |
| if (touchingCondition !== true) { | |
| if (!(xInBounds && yInBounds)) { | |
| return; // Not inside the bounding box. | |
| } | |
| } | |
| // Find the distance to the point for all sides. | |
| // We use this to figure out which side to bounce on. | |
| let nearestEdge = ''; | |
| let minDist = Infinity; | |
| for (let i = 0; i < 4; i++) { | |
| const sides = ['left', 'top', 'right', 'bottom']; | |
| let distx; | |
| let disty; | |
| switch (sides[i]) { | |
| case 'left': | |
| case 'right': | |
| distx = x - bounds[sides[i]]; | |
| disty = y - target.y; | |
| break; | |
| case 'top': | |
| case 'bottom': | |
| distx = x - target.x; | |
| disty = y - bounds[sides[i]]; | |
| break; | |
| } | |
| const distance = Math.sqrt((distx * distx) + (disty * disty)); | |
| if (distance < minDist) { | |
| minDist = distance; | |
| nearestEdge = sides[i]; | |
| } | |
| } | |
| // Point away from the nearest edge. | |
| const radians = MathUtil.degToRad(90 - target.direction); | |
| let dx = Math.cos(radians); | |
| let dy = -Math.sin(radians); | |
| if (nearestEdge === 'left') { | |
| dx = Math.max(0.2, Math.abs(dx)); | |
| } else if (nearestEdge === 'top') { | |
| dy = Math.max(0.2, Math.abs(dy)); | |
| } else if (nearestEdge === 'right') { | |
| dx = 0 - Math.max(0.2, Math.abs(dx)); | |
| } else if (nearestEdge === 'bottom') { | |
| dy = 0 - Math.max(0.2, Math.abs(dy)); | |
| } | |
| const newDirection = MathUtil.radToDeg(Math.atan2(dy, dx)) + 90; | |
| target.setDirection(newDirection); | |
| // Keep within the stage. | |
| const fencedPosition = target.keepInFence(target.x, target.y); | |
| target.setXY(fencedPosition[0], fencedPosition[1]); | |
| } | |
| ifOnSpriteBounce (args, util) { | |
| if (args.SPRITE === '_mouse_') { | |
| const x = util.ioQuery('mouse', 'getScratchX'); | |
| const y = util.ioQuery('mouse', 'getScratchY'); | |
| return this.ifOnXYBounce({ X: x, Y: y }, util); | |
| } else if (args.SPRITE === '_random_') { | |
| const stageWidth = this.runtime.stageWidth; | |
| const stageHeight = this.runtime.stageHeight; | |
| const x = Math.round(stageWidth * (Math.random() - 0.5)); | |
| const y = Math.round(stageHeight * (Math.random() - 0.5)); | |
| return this.ifOnXYBounce({ X: x, Y: y }, util); | |
| } | |
| const spriteName = Cast.toString(args.SPRITE); | |
| const bounceTarget = this.runtime.getSpriteTargetByName(spriteName); | |
| if (!bounceTarget) return; | |
| const point = util.target.spriteTouchingPoint(spriteName); | |
| if (!point) return; | |
| return this.ifOnXYBounce({ X: point[0], Y: point[1] }, util); | |
| } | |
| setRotationStyle (args, util) { | |
| util.target.setRotationStyle(args.STYLE); | |
| } | |
| changeX (args, util) { | |
| const dx = Cast.toNumber(args.DX); | |
| util.target.setXY(util.target.x + dx, util.target.y); | |
| } | |
| setX (args, util) { | |
| const x = Cast.toNumber(args.X); | |
| util.target.setXY(x, util.target.y); | |
| } | |
| changeY (args, util) { | |
| const dy = Cast.toNumber(args.DY); | |
| util.target.setXY(util.target.x, util.target.y + dy); | |
| } | |
| setY (args, util) { | |
| const y = Cast.toNumber(args.Y); | |
| util.target.setXY(util.target.x, y); | |
| } | |
| changeXY (args, util) { | |
| const dx = Cast.toNumber(args.DX); | |
| const dy = Cast.toNumber(args.DY); | |
| util.target.setXY(util.target.x + dx, util.target.y + dy); | |
| } | |
| getX (args, util) { | |
| return this.limitPrecision(util.target.x); | |
| } | |
| getY (args, util) { | |
| return this.limitPrecision(util.target.y); | |
| } | |
| getDirection (args, util) { | |
| return util.target.direction; | |
| } | |
| // This corresponds to snapToInteger in Scratch 2 | |
| limitPrecision (coordinate) { | |
| const rounded = Math.round(coordinate); | |
| const delta = coordinate - rounded; | |
| const limitedCoord = (Math.abs(delta) < 1e-9) ? rounded : coordinate; | |
| return limitedCoord; | |
| } | |
| } | |
| module.exports = Scratch3MotionBlocks; | |