2017-04-17 15:10:04 -04:00
|
|
|
const Cast = require('../util/cast');
|
2018-02-01 19:53:41 -05:00
|
|
|
const Timer = require('../util/timer');
|
2018-11-08 00:37:28 -05:00
|
|
|
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');
|
2016-09-12 10:58:50 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
class Scratch3SensingBlocks {
|
|
|
|
constructor (runtime) {
|
|
|
|
/**
|
|
|
|
* The runtime instantiating this block package.
|
|
|
|
* @type {Runtime}
|
|
|
|
*/
|
|
|
|
this.runtime = runtime;
|
2017-10-31 11:12:26 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The "answer" block value.
|
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
this._answer = '';
|
|
|
|
|
2018-02-06 11:06:48 -05:00
|
|
|
/**
|
|
|
|
* The timer utility.
|
|
|
|
* @type {Timer}
|
|
|
|
*/
|
|
|
|
this._timer = new Timer();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The stored microphone loudness measurement.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
|
|
|
this._cachedLoudness = -1;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The time of the most recent microphone loudness measurement.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
|
|
|
this._cachedLoudnessTimestamp = 0;
|
|
|
|
|
2017-10-31 11:12:26 -04:00
|
|
|
/**
|
|
|
|
* The list of queued questions and respective `resolve` callbacks.
|
|
|
|
* @type {!Array}
|
|
|
|
*/
|
|
|
|
this._questionList = [];
|
|
|
|
|
|
|
|
this.runtime.on('ANSWER', this._onAnswer.bind(this));
|
2017-12-01 20:06:55 -05:00
|
|
|
this.runtime.on('PROJECT_START', this._resetAnswer.bind(this));
|
2017-10-31 11:12:26 -04:00
|
|
|
this.runtime.on('PROJECT_STOP_ALL', this._clearAllQuestions.bind(this));
|
2018-10-02 12:44:13 -04:00
|
|
|
this.runtime.on('STOP_FOR_TARGET', this._clearTargetQuestions.bind(this));
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
2016-08-15 21:37:36 -04:00
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* Retrieve the block primitives implemented by this package.
|
|
|
|
* @return {object.<string, Function>} Mapping of opcode to Function.
|
2016-08-15 21:37:36 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
getPrimitives () {
|
|
|
|
return {
|
|
|
|
sensing_touchingobject: this.touchingObject,
|
|
|
|
sensing_touchingcolor: this.touchingColor,
|
|
|
|
sensing_coloristouchingcolor: this.colorTouchingColor,
|
|
|
|
sensing_distanceto: this.distanceTo,
|
|
|
|
sensing_timer: this.getTimer,
|
|
|
|
sensing_resettimer: this.resetTimer,
|
|
|
|
sensing_of: this.getAttributeOf,
|
|
|
|
sensing_mousex: this.getMouseX,
|
|
|
|
sensing_mousey: this.getMouseY,
|
2017-12-28 12:31:23 -05:00
|
|
|
sensing_setdragmode: this.setDragMode,
|
2017-04-17 19:42:48 -04:00
|
|
|
sensing_mousedown: this.getMouseDown,
|
|
|
|
sensing_keypressed: this.getKeyPressed,
|
|
|
|
sensing_current: this.current,
|
|
|
|
sensing_dayssince2000: this.daysSince2000,
|
2017-10-31 11:12:26 -04:00
|
|
|
sensing_loudness: this.getLoudness,
|
2018-04-30 18:50:13 -04:00
|
|
|
sensing_loud: this.isLoud,
|
2017-10-31 11:12:26 -04:00
|
|
|
sensing_askandwait: this.askAndWait,
|
2018-04-30 20:03:35 -04:00
|
|
|
sensing_answer: this.getAnswer,
|
2018-05-23 11:30:12 -04:00
|
|
|
sensing_username: this.getUsername,
|
2018-05-01 12:58:09 -04:00
|
|
|
sensing_userid: () => {} // legacy no-op block
|
2017-04-17 19:42:48 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-11-15 17:53:43 -05:00
|
|
|
getMonitored () {
|
|
|
|
return {
|
2018-05-08 14:09:18 -04:00
|
|
|
sensing_answer: {
|
|
|
|
getId: () => 'answer'
|
|
|
|
},
|
|
|
|
sensing_loudness: {
|
|
|
|
getId: () => 'loudness'
|
|
|
|
},
|
|
|
|
sensing_timer: {
|
|
|
|
getId: () => 'timer'
|
|
|
|
},
|
|
|
|
sensing_current: {
|
2018-05-09 10:52:00 -04:00
|
|
|
// This is different from the default toolbox xml id in order to support
|
|
|
|
// importing multiple monitors from the same opcode from sb2 files,
|
|
|
|
// something that is not currently supported in scratch 3.
|
2018-11-08 01:33:26 -05:00
|
|
|
getId: (_, fields) => getMonitorIdForBlockWithArgs('current', fields) // _${param}`
|
2018-05-08 14:09:18 -04:00
|
|
|
}
|
2017-11-15 17:53:43 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-10-31 11:12:26 -04:00
|
|
|
_onAnswer (answer) {
|
|
|
|
this._answer = answer;
|
|
|
|
const questionObj = this._questionList.shift();
|
|
|
|
if (questionObj) {
|
2017-11-24 21:28:48 -05:00
|
|
|
const [_question, resolve, target, wasVisible, wasStage] = questionObj;
|
|
|
|
// If the target was visible when asked, hide the say bubble unless the target was the stage.
|
|
|
|
if (wasVisible && !wasStage) {
|
2017-11-08 10:37:38 -05:00
|
|
|
this.runtime.emit('SAY', target, 'say', '');
|
|
|
|
}
|
2017-10-31 11:12:26 -04:00
|
|
|
resolve();
|
|
|
|
this._askNextQuestion();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-01 20:06:55 -05:00
|
|
|
_resetAnswer () {
|
|
|
|
this._answer = '';
|
|
|
|
}
|
|
|
|
|
2017-11-24 21:28:48 -05:00
|
|
|
_enqueueAsk (question, resolve, target, wasVisible, wasStage) {
|
|
|
|
this._questionList.push([question, resolve, target, wasVisible, wasStage]);
|
2017-10-31 11:12:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
_askNextQuestion () {
|
|
|
|
if (this._questionList.length > 0) {
|
2017-11-24 21:28:48 -05:00
|
|
|
const [question, _resolve, target, wasVisible, wasStage] = this._questionList[0];
|
2017-11-08 10:37:38 -05:00
|
|
|
// If the target is visible, emit a blank question and use the
|
2017-11-24 21:28:48 -05:00
|
|
|
// say event to trigger a bubble unless the target was the stage.
|
|
|
|
if (wasVisible && !wasStage) {
|
2017-11-08 10:37:38 -05:00
|
|
|
this.runtime.emit('SAY', target, 'say', question);
|
|
|
|
this.runtime.emit('QUESTION', '');
|
|
|
|
} else {
|
|
|
|
this.runtime.emit('QUESTION', question);
|
|
|
|
}
|
2017-10-31 11:12:26 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_clearAllQuestions () {
|
|
|
|
this._questionList = [];
|
|
|
|
this.runtime.emit('QUESTION', null);
|
|
|
|
}
|
|
|
|
|
2018-10-02 12:44:13 -04:00
|
|
|
_clearTargetQuestions (stopTarget) {
|
|
|
|
const currentlyAsking = this._questionList.length > 0 && this._questionList[0][2] === stopTarget;
|
|
|
|
this._questionList = this._questionList.filter(question => (
|
|
|
|
question[2] !== stopTarget
|
|
|
|
));
|
|
|
|
|
|
|
|
if (currentlyAsking) {
|
2018-12-14 03:35:07 -05:00
|
|
|
this.runtime.emit('SAY', stopTarget, 'say', '');
|
2018-12-11 04:39:09 -05:00
|
|
|
if (this._questionList.length > 0) {
|
|
|
|
this._askNextQuestion();
|
|
|
|
} else {
|
|
|
|
this.runtime.emit('QUESTION', null);
|
|
|
|
}
|
2018-10-02 12:44:13 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-08 10:37:38 -05:00
|
|
|
askAndWait (args, util) {
|
|
|
|
const _target = util.target;
|
2017-10-31 11:12:26 -04:00
|
|
|
return new Promise(resolve => {
|
|
|
|
const isQuestionAsked = this._questionList.length > 0;
|
2017-12-15 15:52:04 -05:00
|
|
|
this._enqueueAsk(String(args.QUESTION), resolve, _target, _target.visible, _target.isStage);
|
2017-10-31 11:12:26 -04:00
|
|
|
if (!isQuestionAsked) {
|
|
|
|
this._askNextQuestion();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
getAnswer () {
|
|
|
|
return this._answer;
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
touchingObject (args, util) {
|
2018-06-07 14:12:23 -04:00
|
|
|
return util.target.isTouchingObject(args.TOUCHINGOBJECTMENU);
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
touchingColor (args, util) {
|
|
|
|
const color = Cast.toRgbColorList(args.COLOR);
|
|
|
|
return util.target.isTouchingColor(color);
|
|
|
|
}
|
|
|
|
|
|
|
|
colorTouchingColor (args, util) {
|
|
|
|
const maskColor = Cast.toRgbColorList(args.COLOR);
|
|
|
|
const targetColor = Cast.toRgbColorList(args.COLOR2);
|
|
|
|
return util.target.colorIsTouchingColor(targetColor, maskColor);
|
|
|
|
}
|
|
|
|
|
|
|
|
distanceTo (args, util) {
|
|
|
|
if (util.target.isStage) return 10000;
|
|
|
|
|
|
|
|
let targetX = 0;
|
|
|
|
let targetY = 0;
|
|
|
|
if (args.DISTANCETOMENU === '_mouse_') {
|
2018-01-09 10:36:03 -05:00
|
|
|
targetX = util.ioQuery('mouse', 'getScratchX');
|
|
|
|
targetY = util.ioQuery('mouse', 'getScratchY');
|
2017-04-17 19:42:48 -04:00
|
|
|
} else {
|
2019-03-06 12:14:59 -05:00
|
|
|
args.DISTANCETOMENU = Cast.toString(args.DISTANCETOMENU);
|
2017-04-17 19:42:48 -04:00
|
|
|
const distTarget = this.runtime.getSpriteTargetByName(
|
|
|
|
args.DISTANCETOMENU
|
|
|
|
);
|
|
|
|
if (!distTarget) return 10000;
|
|
|
|
targetX = distTarget.x;
|
|
|
|
targetY = distTarget.y;
|
2016-10-24 20:37:27 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
|
|
|
|
const dx = util.target.x - targetX;
|
|
|
|
const dy = util.target.y - targetY;
|
|
|
|
return Math.sqrt((dx * dx) + (dy * dy));
|
2016-10-24 20:37:27 -04:00
|
|
|
}
|
|
|
|
|
2017-12-28 12:31:23 -05:00
|
|
|
setDragMode (args, util) {
|
2017-12-28 12:31:23 -05:00
|
|
|
util.target.setDraggable(args.DRAG_MODE === 'draggable');
|
2017-12-28 12:31:23 -05:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
getTimer (args, util) {
|
|
|
|
return util.ioQuery('clock', 'projectTimer');
|
2016-10-24 20:37:27 -04:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
resetTimer (args, util) {
|
|
|
|
util.ioQuery('clock', 'resetProjectTimer');
|
|
|
|
}
|
|
|
|
|
|
|
|
getMouseX (args, util) {
|
2018-01-09 10:36:03 -05:00
|
|
|
return util.ioQuery('mouse', 'getScratchX');
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
getMouseY (args, util) {
|
2018-01-09 10:36:03 -05:00
|
|
|
return util.ioQuery('mouse', 'getScratchY');
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
getMouseDown (args, util) {
|
|
|
|
return util.ioQuery('mouse', 'getIsDown');
|
|
|
|
}
|
|
|
|
|
|
|
|
current (args) {
|
|
|
|
const menuOption = Cast.toString(args.CURRENTMENU).toLowerCase();
|
|
|
|
const date = new Date();
|
|
|
|
switch (menuOption) {
|
|
|
|
case 'year': return date.getFullYear();
|
|
|
|
case 'month': return date.getMonth() + 1; // getMonth is zero-based
|
|
|
|
case 'date': return date.getDate();
|
|
|
|
case 'dayofweek': return date.getDay() + 1; // getDay is zero-based, Sun=0
|
|
|
|
case 'hour': return date.getHours();
|
|
|
|
case 'minute': return date.getMinutes();
|
|
|
|
case 'second': return date.getSeconds();
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
getKeyPressed (args, util) {
|
2017-05-16 00:11:38 -04:00
|
|
|
return util.ioQuery('keyboard', 'getKeyIsDown', [args.KEY_OPTION]);
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
daysSince2000 () {
|
|
|
|
const msPerDay = 24 * 60 * 60 * 1000;
|
|
|
|
const start = new Date(2000, 0, 1); // Months are 0-indexed.
|
|
|
|
const today = new Date();
|
|
|
|
const dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset();
|
|
|
|
let mSecsSinceStart = today.valueOf() - start.valueOf();
|
|
|
|
mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000);
|
|
|
|
return mSecsSinceStart / msPerDay;
|
|
|
|
}
|
|
|
|
|
|
|
|
getLoudness () {
|
|
|
|
if (typeof this.runtime.audioEngine === 'undefined') return -1;
|
2018-02-06 12:25:44 -05:00
|
|
|
if (this.runtime.currentStepTime === null) return -1;
|
2018-02-01 19:53:41 -05:00
|
|
|
|
|
|
|
// Only measure loudness once per step
|
2018-02-06 11:06:48 -05:00
|
|
|
const timeSinceLoudness = this._timer.time() - this._cachedLoudnessTimestamp;
|
|
|
|
if (timeSinceLoudness < this.runtime.currentStepTime) {
|
|
|
|
return this._cachedLoudness;
|
2018-02-01 19:53:41 -05:00
|
|
|
}
|
|
|
|
|
2018-02-06 11:06:48 -05:00
|
|
|
this._cachedLoudnessTimestamp = this._timer.time();
|
|
|
|
this._cachedLoudness = this.runtime.audioEngine.getLoudness();
|
|
|
|
return this._cachedLoudness;
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
2018-04-30 18:50:13 -04:00
|
|
|
isLoud () {
|
|
|
|
return this.getLoudness() > 10;
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
getAttributeOf (args) {
|
|
|
|
let attrTarget;
|
|
|
|
|
|
|
|
if (args.OBJECT === '_stage_') {
|
|
|
|
attrTarget = this.runtime.getTargetForStage();
|
|
|
|
} else {
|
2019-03-06 12:14:59 -05:00
|
|
|
args.OBJECT = Cast.toString(args.OBJECT);
|
2017-04-17 19:42:48 -04:00
|
|
|
attrTarget = this.runtime.getSpriteTargetByName(args.OBJECT);
|
|
|
|
}
|
|
|
|
|
2018-05-03 13:37:45 -04:00
|
|
|
// attrTarget can be undefined if the target does not exist
|
|
|
|
// (e.g. single sprite uploaded from larger project referencing
|
|
|
|
// another sprite that wasn't uploaded)
|
|
|
|
if (!attrTarget) return 0;
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
// Generic attributes
|
|
|
|
if (attrTarget.isStage) {
|
|
|
|
switch (args.PROPERTY) {
|
|
|
|
// Scratch 1.4 support
|
|
|
|
case 'background #': return attrTarget.currentCostume + 1;
|
|
|
|
|
|
|
|
case 'backdrop #': return attrTarget.currentCostume + 1;
|
|
|
|
case 'backdrop name':
|
2018-02-21 19:59:35 -05:00
|
|
|
return attrTarget.getCostumes()[attrTarget.currentCostume].name;
|
2018-03-13 11:45:11 -04:00
|
|
|
case 'volume': return attrTarget.volume;
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
switch (args.PROPERTY) {
|
|
|
|
case 'x position': return attrTarget.x;
|
|
|
|
case 'y position': return attrTarget.y;
|
|
|
|
case 'direction': return attrTarget.direction;
|
|
|
|
case 'costume #': return attrTarget.currentCostume + 1;
|
|
|
|
case 'costume name':
|
2018-02-21 19:59:35 -05:00
|
|
|
return attrTarget.getCostumes()[attrTarget.currentCostume].name;
|
2017-04-17 19:42:48 -04:00
|
|
|
case 'size': return attrTarget.size;
|
2018-03-13 11:45:11 -04:00
|
|
|
case 'volume': return attrTarget.volume;
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-21 11:08:22 -05:00
|
|
|
// Target variables.
|
2017-04-17 19:42:48 -04:00
|
|
|
const varName = args.PROPERTY;
|
2018-11-20 16:32:08 -05:00
|
|
|
const variable = attrTarget.lookupVariableByNameAndType(varName, '', true);
|
|
|
|
if (variable) {
|
|
|
|
return variable.value;
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, 0
|
|
|
|
return 0;
|
|
|
|
}
|
2018-05-23 11:30:12 -04:00
|
|
|
|
2018-07-23 09:38:58 -04:00
|
|
|
getUsername (args, util) {
|
|
|
|
return util.ioQuery('userData', 'getUsername');
|
2018-05-23 11:30:12 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
2016-10-24 20:37:27 -04:00
|
|
|
|
2016-08-15 21:37:36 -04:00
|
|
|
module.exports = Scratch3SensingBlocks;
|