s4s-editor / local-scratch-vm /src /blocks /scratch3_sensing.js
soiz1's picture
Upload 811 files
30c32c8 verified
raw
history blame
22.9 kB
const Cast = require('../util/cast');
const Timer = require('../util/timer');
const MathUtil = require('../util/math-util');
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');
const { validateRegex } = require('../util/json-block-utilities');
class Scratch3SensingBlocks {
constructor (runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
/**
* The "answer" block value.
* @type {string}
*/
this._answer = ''; // used by compiler
/**
* 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;
/**
* The list of loudness values to determine the average.
* @type {!Array}
*/
this._loudnessList = [];
/**
* The list of queued questions and respective `resolve` callbacks.
* @type {!Array}
*/
this._questionList = [];
this.runtime.on('ANSWER', this._onAnswer.bind(this));
this.runtime.on('PROJECT_START', this._resetAnswer.bind(this));
this.runtime.on('PROJECT_STOP_ALL', this._clearAllQuestions.bind(this));
this.runtime.on('STOP_FOR_TARGET', this._clearTargetQuestions.bind(this));
this.runtime.on('RUNTIME_DISPOSED', this._resetAnswer.bind(this));
}
/**
* Retrieve the block primitives implemented by this package.
* @return {object.<string, Function>} Mapping of opcode to Function.
*/
getPrimitives () {
return {
sensing_objecttouchingobject: this.objectTouchingObject,
sensing_objecttouchingclonesprite: this.objectTouchingCloneOfSprite,
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,
sensing_setdragmode: this.setDragMode,
sensing_mousedown: this.getMouseDown,
sensing_keypressed: this.getKeyPressed,
sensing_current: this.current,
sensing_dayssince2000: this.daysSince2000,
sensing_loudness: this.getLoudness,
sensing_loud: this.isLoud,
sensing_askandwait: this.askAndWait,
sensing_answer: this.getAnswer,
sensing_username: this.getUsername,
sensing_loggedin: this.getLoggedIn,
sensing_userid: () => {}, // legacy no-op block
sensing_regextest: this.regextest,
sensing_thing_is_number: this.thing_is_number,
sensing_thing_has_number: this.thing_has_number,
sensing_mobile: this.mobile,
sensing_thing_is_text: this.thing_is_text,
sensing_getspritewithattrib: this.getspritewithattrib,
sensing_directionTo: this.getDirectionToFrom,
sensing_distanceTo: this.getDistanceToFrom,
sensing_isUpperCase: this.isCharecterUppercase,
sensing_mouseclicked: this.mouseClicked,
sensing_keyhit: this.keyHit,
sensing_mousescrolling: this.mouseScrolling,
sensing_fingerdown: this.fingerDown,
sensing_fingertapped: this.fingerTapped,
sensing_fingerx: this.getFingerX,
sensing_fingery: this.getFingerY,
sensing_setclipboard: this.setClipboard,
sensing_getclipboard: this.getClipboard,
sensing_getdragmode: this.getDragMode,
sensing_getoperatingsystem: this.getOS,
sensing_getbrowser: this.getBrowser,
sensing_geturl: this.getUrl,
sensing_getxyoftouchingsprite: this.getXYOfTouchingSprite
};
}
getOS () {
if (!('userAgent' in navigator)) return 'Unknown';
const agent = navigator.userAgent;
if (agent.includes('Windows')) {
return 'Windows';
}
if (agent.includes('Android')) {
return 'Android';
}
if (agent.includes('iPad') || agent.includes('iPod') || agent.includes('iPhone')) {
return 'iOS';
}
if (agent.includes('Linux')) {
return 'Linux';
}
if (agent.includes('CrOS')) {
return 'ChromeOS';
}
if (agent.includes('Mac OS')) {
return 'MacOS';
}
return 'Unknown';
}
getBrowser () {
if (!('userAgent' in navigator)) return 'Unknown';
const agent = navigator.userAgent;
if ('userAgentData' in navigator) {
const agentData = JSON.stringify(navigator.userAgentData.brands);
if (agentData.includes('Google Chrome')) {
return 'Chrome';
}
if (agentData.includes('Opera')) {
return 'Opera';
}
if (agentData.includes('Microsoft Edge')) {
return 'Edge';
}
}
if (agent.includes('Chrome')) {
return 'Chrome';
}
if (agent.includes('Firefox')) {
return 'Firefox';
}
// PenguinMod cannot be loaded in IE 11 (the last supported version)
// if (agent.includes('MSIE') || agent.includes('rv:')) {
// return 'Internet Explorer';
// }
if (agent.includes('Safari')) {
return 'Safari';
}
return 'Unknown';
}
getUrl () {
if (!('href' in location)) return '';
return location.href;
}
setClipboard (args) {
const text = Cast.toString(args.ITEM);
if (!navigator) return;
if (('clipboard' in navigator) && ('writeText' in navigator.clipboard)) {
navigator.clipboard.writeText(text);
}
}
getClipboard () {
if (!navigator) return '';
if (('clipboard' in navigator) && ('readText' in navigator.clipboard)) {
return navigator.clipboard.readText();
} else {
return '';
}
}
getDragMode (_, util) {
return util.target.draggable;
}
mouseClicked (_, util) {
return util.ioQuery('mouse', 'getIsClicked');
}
keyHit (args, util) {
return util.ioQuery('keyboard', 'getKeyIsHit', [args.KEY_OPTION]);
}
mouseScrolling (args, util) {
const delta = util.ioQuery('mouseWheel', 'getScrollDelta');
const option = args.SCROLL_OPTION;
switch (option) {
case "up":
return delta < 0;
case "down":
return delta > 0;
default:
return false;
}
}
isCharecterUppercase (args) {
return (/[A-Z]/g).test(args.text);
}
getDirectionToFrom (args) {
const dx = args.x2 - args.x1;
const dy = args.y2 - args.y1;
const direction = MathUtil.wrapClamp(90 - MathUtil.radToDeg(Math.atan2(dy, dx)), -179, 180);
return direction;
}
getDistanceToFrom (args) {
const dx = args.x2 - args.x1;
const dy = args.y2 - args.y1;
return Math.sqrt((dx * dx) + (dy * dy));
}
getspritewithattrib (args, util) {
// strip out usless data
const sprites = util.runtime.targets.map(x => ({
id: x.id,
name: x.sprite ? x.sprite.name : "Unknown",
variables: Object.values(x.variables).reduce((obj, value) => {
if (!value.name) return obj;
obj[value.name] = String(value.value);
return obj;
}, {})
}));
// get the target with variable x set to y
let res = "No sprites found";
for (
// define the index and the sprite
let idx = 1, sprite = sprites[0];
// standard for loop thing
idx < sprites.length;
// set sprite to a new item
sprite = sprites[idx++]
) {
if (sprite.variables[args.var] === args.val) {
res = `{"id": "${sprite.id}", "name": "${sprite.name}"}`;
break;
}
}
return res;
}
thing_is_number (args) {
// i hate js
// i also hate regex
// so im gonna do this the lazy way
// no. String(Number(value)) === value does infact do the job X)
// also what was originaly here was inificiant as hell
// jg: why dont you literally just do what "is text" did but the opposite
// except also account for numbers that end with . (that aint a number)
if (Cast.toString(args.TEXT1).trim().endsWith(".")) {
return false;
}
return !this.thing_is_text(args);
}
thing_is_text (args) {
// WHY IS NAN NOT EQUAL TO ITSELF
// HOW IS NAN A NUMBER
// because nan is how numbers say the value put into me is not a number
return isNaN(Number(args.TEXT1));
}
thing_has_number(args) {
return /\d/.test(Cast.toString(args.TEXT1));
}
mobile () {
return typeof window !== 'undefined' && 'ontouchstart' in window;
}
regextest (args) {
if (!validateRegex(args.reg, args.regrule)) return false;
const regex = new RegExp(args.reg, args.regrule);
return regex.test(args.text);
}
getMonitored () {
return {
sensing_answer: {
getId: () => 'answer'
},
sensing_mousedown: {
getId: () => 'mousedown'
},
sensing_mouseclicked: {
getId: () => 'mouseclicked'
},
sensing_mousex: {
getId: () => 'mousex'
},
sensing_mousey: {
getId: () => 'mousey'
},
sensing_getclipboard: {
getId: () => 'getclipboard'
},
sensing_getdragmode: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_getdragmode`
},
sensing_loudness: {
getId: () => 'loudness'
},
sensing_loud: {
getId: () => 'loud'
},
sensing_timer: {
getId: () => 'timer'
},
sensing_dayssince2000: {
getId: () => 'dayssince2000'
},
sensing_current: {
// 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.
getId: (_, fields) => getMonitorIdForBlockWithArgs('current', fields) // _${param}`
},
sensing_loggedin: {
getId: () => 'loggedin'
},
};
}
_onAnswer (answer) {
this._answer = answer;
const questionObj = this._questionList.shift();
if (questionObj) {
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) {
this.runtime.emit('SAY', target, 'say', '');
}
resolve();
this._askNextQuestion();
}
}
_resetAnswer () {
this._answer = '';
}
_enqueueAsk (question, resolve, target, wasVisible, wasStage) {
this._questionList.push([question, resolve, target, wasVisible, wasStage]);
}
_askNextQuestion () {
if (this._questionList.length > 0) {
const [question, _resolve, target, wasVisible, wasStage] = this._questionList[0];
// If the target is visible, emit a blank question and use the
// say event to trigger a bubble unless the target was the stage.
if (wasVisible && !wasStage) {
this.runtime.emit('SAY', target, 'say', question);
this.runtime.emit('QUESTION', '');
} else {
this.runtime.emit('QUESTION', question);
}
}
}
_clearAllQuestions () {
this._questionList = [];
this.runtime.emit('QUESTION', null);
}
_clearTargetQuestions (stopTarget) {
const currentlyAsking = this._questionList.length > 0 && this._questionList[0][2] === stopTarget;
this._questionList = this._questionList.filter(question => (
question[2] !== stopTarget
));
if (currentlyAsking) {
this.runtime.emit('SAY', stopTarget, 'say', '');
if (this._questionList.length > 0) {
this._askNextQuestion();
} else {
this.runtime.emit('QUESTION', null);
}
}
}
askAndWait (args, util) {
const _target = util.target;
return new Promise(resolve => {
const isQuestionAsked = this._questionList.length > 0;
this._enqueueAsk(String(args.QUESTION), resolve, _target, _target.visible, _target.isStage);
if (!isQuestionAsked) {
this._askNextQuestion();
}
});
}
getAnswer () {
return this._answer;
}
objectTouchingObject (args, util) {
const object1 = (args.FULLTOUCHINGOBJECTMENU) === "_myself_" ? util.target.getName() : args.FULLTOUCHINGOBJECTMENU;
const object2 = args.SPRITETOUCHINGOBJECTMENU;
if (object2 === "_myself_") {
return util.target.isTouchingObject(object1);
}
const target = this.runtime.getSpriteTargetByName(object2);
if (!target) return false;
return target.isTouchingObject(object1);
}
objectTouchingCloneOfSprite (args, util) {
const object1 = args.FULLTOUCHINGOBJECTMENU;
let object2 = args.SPRITETOUCHINGOBJECTMENU;
if (object2 === "_myself_") {
object2 = util.target.getName();
}
if (object1 === "_myself_") {
return util.target.isTouchingObject(object2, true);
}
const target = this.runtime.getSpriteTargetByName(object2);
if (!target) return false;
if (object1 === "_mouse_") {
if (!this.runtime.ioDevices.mouse) return false;
const mouseX = this.runtime.ioDevices.mouse.getClientX();
const mouseY = this.runtime.ioDevices.mouse.getClientY();
const clones = target.sprite.clones.filter(clone => !clone.isOriginal && clone.isTouchingPoint(mouseX, mouseY));
return clones.length > 0;
} else if (object1 === '_edge_') {
const clones = target.sprite.clones.filter(clone => !clone.isOriginal && clone.isTouchingEdge());
return clones.length > 0;
}
const originalSprite = this.runtime.getSpriteTargetByName(object1);
if (!originalSprite) return false;
return originalSprite.isTouchingObject(object2, true);
}
touchingObject (args, util) {
return util.target.isTouchingObject(args.TOUCHINGOBJECTMENU);
}
getXYOfTouchingSprite (args, util) {
const object = args.SPRITE;
if (object === '_mouse_') {
// we can just return mouse pos
// if mouse is touching us, the mouse size is practically 1x1 anyways
const x = util.ioQuery('mouse', 'getScratchX');
const y = util.ioQuery('mouse', 'getScratchY');
if (args.XY === 'y') return y;
return x;
}
const point = util.target.spriteTouchingPoint(object);
if (!point) return '';
if (args.XY === 'y') return point[1];
return point[0];
}
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_') {
targetX = util.ioQuery('mouse', 'getScratchX');
targetY = util.ioQuery('mouse', 'getScratchY');
} else {
args.DISTANCETOMENU = Cast.toString(args.DISTANCETOMENU);
const distTarget = this.runtime.getSpriteTargetByName(
args.DISTANCETOMENU
);
if (!distTarget) return 10000;
targetX = distTarget.x;
targetY = distTarget.y;
}
const dx = util.target.x - targetX;
const dy = util.target.y - targetY;
return Math.sqrt((dx * dx) + (dy * dy));
}
setDragMode (args, util) {
util.target.setDraggable(args.DRAG_MODE === 'draggable');
}
getTimer (args, util) {
return util.ioQuery('clock', 'projectTimer');
}
resetTimer (args, util) {
util.ioQuery('clock', 'resetProjectTimer');
}
getMouseX (args, util) {
return util.ioQuery('mouse', 'getScratchX');
}
getMouseY (args, util) {
return util.ioQuery('mouse', 'getScratchY');
}
getMouseDown (args, util) {
return util.ioQuery('mouse', 'getIsDown');
}
getFingerX (args, util) {
return util.ioQuery('touch', 'getScratchX', [Cast.toNumber(args.FINGER_OPTION) - 1]);
}
getFingerY (args, util) {
return util.ioQuery('touch', 'getScratchY', [Cast.toNumber(args.FINGER_OPTION) - 1]);
}
fingerDown (args, util) {
return util.ioQuery('touch', 'getIsDown', [Cast.toNumber(args.FINGER_OPTION) - 1]);
}
fingerTapped (args, util) {
return util.ioQuery('touch', 'getIsTapped', [Cast.toNumber(args.FINGER_OPTION) - 1]);
}
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) {
return util.ioQuery('keyboard', 'getKeyIsDown', [args.KEY_OPTION]);
}
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;
if (this.runtime.currentStepTime === null) return -1;
// Only measure loudness once per step
const timeSinceLoudness = this._timer.time() - this._cachedLoudnessTimestamp;
if (timeSinceLoudness < this.runtime.currentStepTime) {
return this._cachedLoudness;
}
this._cachedLoudnessTimestamp = this._timer.time();
this._cachedLoudness = this.runtime.audioEngine.getLoudness();
this.pushLoudness(this._cachedLoudness);
return this._cachedLoudness;
}
isLoud () {
this.pushLoudness();
let sum = this._loudnessList.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
sum /= this._loudnessList.length;
return this.getLoudness() > sum + 15;
}
pushLoudness (value) {
if (this._loudnessList.length >= 30) this._loudnessList.shift(); // remove first item
this._loudnessList.push(value ?? this.getLoudness());
}
getAttributeOf (args) {
let attrTarget;
if (args.OBJECT === '_stage_') {
attrTarget = this.runtime.getTargetForStage();
} else {
args.OBJECT = Cast.toString(args.OBJECT);
attrTarget = this.runtime.getSpriteTargetByName(args.OBJECT);
}
// 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;
// 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':
return attrTarget.getCostumes()[attrTarget.currentCostume].name;
case 'volume': return attrTarget.volume;
}
} 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':
return attrTarget.getCostumes()[attrTarget.currentCostume].name;
case 'layer': return attrTarget.getLayerOrder();
case 'size': return attrTarget.size;
case 'volume': return attrTarget.volume;
}
}
// Target variables.
const varName = args.PROPERTY;
const variable = attrTarget.lookupVariableByNameAndType(varName, '', true);
if (variable) {
return variable.value;
}
// Otherwise, 0
return 0;
}
getUsername (args, util) {
return util.ioQuery('userData', 'getUsername');
}
getLoggedIn(args, util) {
return util.ioQuery('userData', 'getLoggedIn');
}
}
module.exports = Scratch3SensingBlocks;