Make "when clicked" blocks target dependent.

This involves adding a new opcode, event_whenstageclicked, and adding a
method to the blocks container to update the opcode pair depending on
whether the target is the stage. This method is then called in two
places: first after the sb2 import parses the blocks (not done in the
sb2 parsing itself because no other blocks are target specific) and then
again when blocks are shared between targets.

Also added tests for the block container method, and a fixture project
that tests the opcode splitting on sb2 import.
This commit is contained in:
Paul Kaplan 2018-03-20 09:50:57 -04:00
parent 2b985fa083
commit 140094a7ad
8 changed files with 82 additions and 0 deletions

View file

@ -32,6 +32,9 @@ class Scratch3EventBlocks {
event_whenthisspriteclicked: {
restartExistingThreads: true
},
event_whenstageclicked: {
restartExistingThreads: true
},
event_whenbackdropswitchesto: {
restartExistingThreads: true
},

View file

@ -593,6 +593,21 @@ class Blocks {
}
}
/**
* Keep blocks up to date after they are shared between targets.
* @param {boolean} isStage If the new target is a stage.
*/
updateTargetSpecificBlocks (isStage) {
const blocks = this._blocks;
for (const blockId in blocks) {
if (isStage && blocks[blockId].opcode === 'event_whenthisspriteclicked') {
blocks[blockId].opcode = 'event_whenstageclicked';
} else if (!isStage && blocks[blockId].opcode === 'event_whenstageclicked') {
blocks[blockId].opcode = 'event_whenthisspriteclicked';
}
}
}
/**
* Update blocks after a sound, costume, or backdrop gets renamed.
* Any block referring to the old name of the asset should get updated

View file

@ -31,8 +31,15 @@ class Mouse {
// only activate click hat if the mouse up event wasn't
// the result of a drag ending
if (!wasDragged) {
// Activate both "this sprite clicked" and "stage clicked"
// They were separated into two opcodes for labeling,
// but should act the same way.
// Intentionally not checking isStage to make it work when sharing blocks.
// @todo the blocks should be converted from one to another when shared
this.runtime.startHats('event_whenthisspriteclicked',
null, target);
this.runtime.startHats('event_whenstageclicked',
null, target);
}
return;
}

View file

@ -275,6 +275,9 @@ const parseScratchObject = function (object, runtime, extensions, topLevel) {
parseScripts(object.scripts, blocks, addBroadcastMsg, getVariableId, extensions);
}
// Update stage specific blocks (e.g. sprite clicked <=> stage clicked)
blocks.updateTargetSpecificBlocks(topLevel); // topLevel = isStage
if (object.hasOwnProperty('lists')) {
for (let k = 0; k < object.lists.length; k++) {
const list = object.lists[k];

View file

@ -778,6 +778,7 @@ class VirtualMachine extends EventEmitter {
for (let i = 0; i < blocks.length; i++) {
target.blocks.createBlock(blocks[i]);
}
target.blocks.updateTargetSpecificBlocks(target.isStage);
}
/**

BIN
test/fixtures/when-clicked.sb2 vendored Normal file

Binary file not shown.

View file

@ -746,3 +746,33 @@ test('updateAssetName doesn\'t update name if name isn\'t being used', t => {
t.equals(b.getBlock('id1').fields.BACKDROP.value, 'foo');
t.end();
});
test('updateTargetSpecificBlocks changes sprite clicked hat to stage clicked for stage', t => {
const b = new Blocks();
b.createBlock({
id: 'originallySpriteClicked',
opcode: 'event_whenthisspriteclicked'
});
b.createBlock({
id: 'originallyStageClicked',
opcode: 'event_whenstageclicked'
});
// originallySpriteClicked does not update when on a non-stage target
b.updateTargetSpecificBlocks(false /* isStage */);
t.equals(b.getBlock('originallySpriteClicked').opcode, 'event_whenthisspriteclicked');
// originallySpriteClicked does update when on a stage target
b.updateTargetSpecificBlocks(true /* isStage */);
t.equals(b.getBlock('originallySpriteClicked').opcode, 'event_whenstageclicked');
// originallyStageClicked does not update when on a stage target
b.updateTargetSpecificBlocks(true /* isStage */);
t.equals(b.getBlock('originallyStageClicked').opcode, 'event_whenstageclicked');
// originallyStageClicked does update when on a non-stage target
b.updateTargetSpecificBlocks(false/* isStage */);
t.equals(b.getBlock('originallyStageClicked').opcode, 'event_whenthisspriteclicked');
t.end();
});

View file

@ -68,3 +68,26 @@ test('data scoping', t => {
t.end();
});
});
test('whenclicked blocks imported separately', t => {
// This sb2 fixture has a single "whenClicked" block on both sprite and stage
const uri = path.resolve(__dirname, '../fixtures/when-clicked.sb2');
const file = extract(uri);
const json = JSON.parse(file);
// Create runtime instance & load SB2 into it
const rt = new Runtime();
sb2.deserialize(json, rt).then(({targets}) => {
const stage = targets[0];
t.equal(stage.isStage, true); // Make sure we have the correct target
const stageOpcode = stage.blocks.getBlock(stage.blocks.getScripts()[0]).opcode;
t.equal(stageOpcode, 'event_whenstageclicked');
const sprite = targets[1];
t.equal(sprite.isStage, false); // Make sure we have the correct target
const spriteOpcode = sprite.blocks.getBlock(sprite.blocks.getScripts()[0]).opcode;
t.equal(spriteOpcode, 'event_whenthisspriteclicked');
t.end();
});
});