scratch-vm/src/sprites/sprite.js

173 lines
5.8 KiB
JavaScript
Raw Normal View History

2017-04-17 15:10:04 -04:00
const RenderedTarget = require('./rendered-target');
const Blocks = require('../engine/blocks');
2017-09-07 11:51:21 -04:00
const {loadSoundFromAsset} = require('../import/load-sound');
const {loadCostumeFromAsset} = require('../import/load-costume');
const StringUtil = require('../util/string-util');
const StageLayering = require('../engine/stage-layering');
2017-04-17 19:42:48 -04:00
class Sprite {
/**
2017-04-17 19:42:48 -04:00
* Sprite to be used on the Scratch stage.
2018-06-15 12:22:49 -04:00
* All clones of a sprite have shared blocks, shared costumes, shared variables,
* shared sounds, etc.
2017-04-17 19:42:48 -04:00
* @param {?Blocks} blocks Shared blocks object for all clones of sprite.
* @param {Runtime} runtime Reference to the runtime.
* @constructor
*/
constructor (blocks, runtime) {
2017-04-17 19:42:48 -04:00
this.runtime = runtime;
if (!blocks) {
// Shared set of blocks for all clones.
blocks = new Blocks();
}
this.blocks = blocks;
/**
* Human-readable name for this sprite (and all clones).
* @type {string}
*/
this.name = '';
/**
* List of costumes for this sprite.
* Each entry is an object, e.g.,
* {
* skinId: 1,
* name: "Costume Name",
* bitmapResolution: 2,
* rotationCenterX: 0,
2018-02-23 10:21:29 -05:00
* rotationCenterY: 0
2017-04-17 19:42:48 -04:00
* }
* @type {Array.<!Object>}
*/
this.costumes_ = [];
2017-04-17 19:42:48 -04:00
/**
* List of sounds for this sprite.
*/
this.sounds = [];
/**
* List of clones for this sprite, including the original.
* @type {Array.<!RenderedTarget>}
*/
this.clones = [];
2018-06-15 12:22:49 -04:00
this.soundBank = null;
if (this.runtime && this.runtime.audioEngine) {
this.soundBank = this.runtime.audioEngine.createBank();
}
}
2018-05-15 22:22:44 -04:00
/**
* Add an array of costumes, taking care to avoid duplicate names.
* @param {!Array<object>} costumes Array of objects representing costumes.
*/
2018-02-22 15:42:35 -05:00
set costumes (costumes) {
this.costumes_ = [];
for (const costume of costumes) {
this.addCostumeAt(costume, this.costumes_.length);
}
}
2018-02-22 15:42:35 -05:00
/**
* Get full costume list
* @return {object[]} list of costumes. Note that mutating the returned list will not
* mutate the list on the sprite. The sprite list should be mutated by calling
* addCostumeAt, deleteCostumeAt, or setting costumes.
*/
get costumes () {
return this.costumes_;
2018-02-22 15:42:35 -05:00
}
/**
* Add a costume at the given index, taking care to avoid duplicate names.
* @param {!object} costumeObject Object representing the costume.
* @param {!int} index Index at which to add costume
*/
addCostumeAt (costumeObject, index) {
2018-02-22 15:42:35 -05:00
if (!costumeObject.name) {
costumeObject.name = '';
}
const usedNames = this.costumes_.map(costume => costume.name);
costumeObject.name = StringUtil.unusedName(costumeObject.name, usedNames);
this.costumes_.splice(index, 0, costumeObject);
}
/**
* Delete a costume by index.
* @param {number} index Costume index to be deleted
*/
2018-02-22 15:42:35 -05:00
deleteCostumeAt (index) {
this.costumes_ = this.costumes_
.slice(0, index)
.concat(this.costumes_.slice(index + 1));
2017-04-17 19:42:48 -04:00
}
/**
2017-04-17 19:42:48 -04:00
* Create a clone of this sprite.
* @param {string=} optLayerGroup Optional layer group the clone's drawable should be added to
* Defaults to the sprite layer group
2017-04-17 19:42:48 -04:00
* @returns {!RenderedTarget} Newly created clone.
*/
createClone (optLayerGroup) {
2017-04-17 19:42:48 -04:00
const newClone = new RenderedTarget(this, this.runtime);
newClone.isOriginal = this.clones.length === 0;
this.clones.push(newClone);
2018-01-09 15:57:33 -05:00
newClone.initAudio();
2017-04-17 19:42:48 -04:00
if (newClone.isOriginal) {
// Default to the sprite layer group if optLayerGroup is not provided
const layerGroup = typeof optLayerGroup === 'string' ? optLayerGroup : StageLayering.SPRITE_LAYER;
newClone.initDrawable(layerGroup);
this.runtime.fireTargetWasCreated(newClone);
} else {
this.runtime.fireTargetWasCreated(newClone, this.clones[0]);
2017-04-17 19:42:48 -04:00
}
return newClone;
}
/**
* Disconnect a clone from this sprite. The clone is unmodified.
* In particular, the clone's dispose() method is not called.
* @param {!RenderedTarget} clone - the clone to be removed.
*/
removeClone (clone) {
2017-10-06 13:43:07 -04:00
this.runtime.fireTargetWasRemoved(clone);
const cloneIndex = this.clones.indexOf(clone);
if (cloneIndex >= 0) {
this.clones.splice(cloneIndex, 1);
}
}
2017-09-07 11:51:21 -04:00
duplicate () {
const newSprite = new Sprite(null, this.runtime);
newSprite.blocks = this.blocks.duplicate();
const allNames = this.runtime.targets.map(t => t.sprite.name);
2017-09-07 11:51:21 -04:00
newSprite.name = StringUtil.unusedName(this.name, allNames);
const assetPromises = [];
2018-02-22 15:42:35 -05:00
newSprite.costumes = this.costumes_.map(costume => {
2017-09-07 11:51:21 -04:00
const newCostume = Object.assign({}, costume);
const costumeAsset = this.runtime.storage.get(costume.assetId);
assetPromises.push(loadCostumeFromAsset(newCostume, costumeAsset, this.runtime));
return newCostume;
2018-02-22 15:42:35 -05:00
});
2017-09-07 11:51:21 -04:00
newSprite.sounds = this.sounds.map(sound => {
const newSound = Object.assign({}, sound);
const soundAsset = this.runtime.storage.get(sound.assetId);
assetPromises.push(loadSoundFromAsset(newSound, soundAsset, this.runtime, this));
2017-09-07 11:51:21 -04:00
return newSound;
});
return Promise.all(assetPromises).then(() => newSprite);
}
2018-06-15 12:22:49 -04:00
dispose () {
if (this.soundBank) {
this.soundBank.dispose();
}
}
2017-04-17 19:42:48 -04:00
}
module.exports = Sprite;