Newer
Older
lostmynuts / shared / js / Engine / Display / PlayWithCallback.js
/* Handles any calls to PlayWithCallback.play, tracking the 'cancel' callbacks,
 * and automatically calling them in the event that the entire set gets cancelled.
 * This can help prevent spurious callbacks from messing with the animation state
 * form FormationCharacterAnimations when the animation is cancelled,
 * and the next animation sets up new sequences (which may cause the PlayWithCallback instances
 * from the previous animation to get called, even though that animation has been deactivated)
 */
class ManagedAnimationCallbackSet
{
	constructor()
	{
		this.cancels = [];
	}
	// plays a new animation, and cancels the callbacks currently waiting
	// (assumedly, we have moved on from the animation state that those
	// callbacks represented, and we no longer want to process the actions
	// that should occur at the end of those animations)
	playNew(g /*Drawable*/,
			sequence /*int*/,
			callback /*Action<bool>*/,
			playMode /*= Engine.DrawableStatics.PlayMode.Forwards*/,
			startFrame /*= 0*/,
			endFrame /*int? = null*/) /*function(bool)*/
	{
		this.cancel();
		return this.play(g, sequence, callback, playMode, startFrame, endFrame);
	}
	
	// Plays a new animation on a clip, and calls the given callback when the animation
	// completes or is interrupted. If cancel() is called on the entire set, the
	// given callback will never be called (presumably the state of the animation
	// has changed such that the given callback is no longer valid, for instance, the owner
	// FormationCharacterAnimation has been deactivate()'ed)
	play(g /*Drawable*/,
		 sequence /*int*/,
		 callback /*Action<bool>*/,
		 playMode /*= Engine.DrawableStatics.PlayMode.Forwards*/,
		 startFrame /*= 0*/,
		 endFrame /*int? = null*/) /*function(bool)*/
	{
		var newCallback = (finished, cancel) =>
		{
			this.cancels.remove(cancel);
			if (callback != null) callback(finished, cancel);
		};
		var cancel = PlayWithCallback.play(g, sequence, newCallback, playMode, startFrame, endFrame);
		this.cancels.push(cancel);
		return cancel;
	}
	
	cancel()
	{
		for (var c of this.cancels)
		{
			c(false);
		}
		
		this.cancels.length = 0;
	}
}


var PlayWithCallback = {};

/// <summary>
/// Play the given sequence once through on the given drawable (either forwards or backwards)
/// Calls a callback with true/false depending on whether the sequence plays:
///  - graphic failed to load = false (immediate/on load failed)
///  - no such sequence = false (immediate)
///  - sequence has only 1 frame = true (immediate)
///  - sequence plays through = true (after finished)
///  - sequence interrupted = false (when interrupted)
/// 
/// Returns a callback you can call to cancel the action (param controls whether or not you still get the callback)
/// </summary>
/// <param name="g"></param>
/// <param name="sequence"></param>
/// <param name="callback"></param>
/// <param name="playMode"></param>
/// <returns></returns>
PlayWithCallback.play = function(g /*Drawable*/,
								 sequence /*int*/,
								 callback /*Action<bool>*/,
								 playMode /*= Engine.DrawableStatics.PlayMode.Forwards*/,
								 startFrame /*= 0*/,
								 endFrame /*int? = null*/) /*function(bool)*/
{
	if (playMode === undefined) playMode = Engine.DrawableStatics.PlayMode.Forwards;
	if (startFrame === undefined) startFrame = 0;
	if (endFrame === undefined) endFrame = null;
	
	var pwc = PlayWithCallback.pool.getNextObject();

	var activationNumber = pwc.activationNumber;

	pwc.init(g, sequence, callback, playMode, startFrame, endFrame);
	var cancel = b => pwc.cancel(activationNumber, b);
	pwc.lastCancel = cancel;
	return cancel;
}

class PlayWithCallbackInternal
{
	constructor()
	{
		this.graphicDef = null;
		this.g = null;	//Drawable
		this.sequence = 0;
		this.startFrame = 0;
		this.endFrame = null;	//int?
		this.callback = null;	//function(bool)
		this.playMode = Engine.DrawableStatics.PlayMode.Forwards;

		this.active = false;
		this.activationNumber = 0;
		this.lastStopAtEnd = null;	//bool?
		
		this.frameEventReference = null;
		
		this.lastCancel = null;	// last cancel callback
	}	

	init(g /*Drawable*/, sequence, callback /*function(bool)*/, playMode, startFrame, endFrame /*int?*/)
	{
		this.active = true;

		this.g = g;
		this.graphicDef = g.graphicData;
		this.sequence = sequence;
		this.callback = callback;
		this.playMode = playMode;
		this.startFrame = startFrame;
		this.endFrame = endFrame;
		this.lastStopAtEnd = null;

		Engine.EnterFrameManagerInstance.add(this, this.update);

		this.go();
	}

	update(timeElapsed)
	{
		if (this.active && (this.g.getSequence() != this.sequence || this.g.graphicData != this.graphicDef))
		{
			this.done(true, false);
		}
	}

	go()
	{
		this.frameEventReference = null;
		
		if (this.g.graphicData == null)
		{
			this.done(true, false);
			return;
		}

		if (this.g.graphicData.loadStatus == GraphicDefStatics.LoadState.None)
		{
			this.graphicDef.onChange.removeListener(this);
			this.graphicDef.onChange.addListener(this, this.graphicChanged);
			return;
		}

		if (this.g.numSequences > this.sequence)
		{
			var frame = this.playMode == Engine.DrawableStatics.PlayMode.Forwards ? this.startFrame : -1 - this.startFrame;

			this.g.setSequence(this.sequence);
			if (this.endFrame != null) this.endFrame = Math.min(this.g.totalFrames - 1, this.endFrame);
			if (Engine.Utils.getValueOrDefault(this.endFrame, this.g.totalFrames - 1) > this.startFrame)
			{
				this.lastStopAtEnd = this.g.stopAtEnd;

				if (this.endFrame != null)
				{
					this.frameEventReference = this.animationCompleted.bind(this);
					this.g.setFrameEvent(this.g.getSequence(), this.endFrame, this.frameEventReference, true);
				}
				else
				{
					this.g.stopAtEnd = true;
					this.g.animationCompleted.addListener(this, this.animationCompleted);
				}

				this.g.gotoAndPlay(frame, this.playMode);
			}
			else
			{
				this.g.gotoAndStop(frame);
				this.done(true, true);
			}
		}
		else
		{
			this.done(true, false);
		}
	}

	animationCompleted(obj)
	{
		obj.stop();
		this.done(true, this.g.getSequence() == this.sequence);
	}

	graphicChanged(graphicDef)
	{
		this.go();
	}

	done(makeCallback, played)
	{
		if (!this.active) return;

		Engine.EnterFrameManagerInstance.remove(this);

		this.active = false;
		this.activationNumber++;

		if (this.endFrame != null)
		{
			if (this.frameEventReference != null)
			{
				this.g.removeFrameEvent(this.g.getSequence(), this.endFrame, this.frameEventReference);
				this.frameEventReference = null;
			}
		}
		else
		{
			this.g.animationCompleted.removeListener(this);
		}
		this.graphicDef.onChange.removeListener(this);

		this.g = null;
		this.graphicDef = null;
		
		var tmpCancel = this.lastCancel;
		this.lastCancel = null;
		
		var tmp = this.callback;
		this.callback = null;

		PlayWithCallback.pool.returnObject(this);

		if (makeCallback)
		{
			if (tmp != null) tmp(played, tmpCancel);
		}
	}

	cancel(activationNumber, stillMakeCallback)
	{
		if (this.active && this.activationNumber == activationNumber)
		{
			this.done(stillMakeCallback, false);
		}
	}
}

PlayWithCallback.pool = new Engine.ObjectPool(PlayWithCallbackInternal);