/* Scenario Cutscene System
   Version 1.2.1
   © 2008 Bruce Pascoe
   
   CHANGELOG:
   
   1.2.1 - Tuesday, March 10, 2009
     [*] Scenelets can no longer have negative priority (reserved for internal
         threads)
     [*] Executing a scenelet no longer interferes with already-running scenelets
     [*] Removed redundant COMMAND_ANIMATE from walk routine
     [+] Built-in scenelets hide, show
   
   1.2 - Saturday, September 6, 2008
     [!] walk parameters reordered
     [!] Poorly named Strafe parameter of walk changed to FaceFirst (meaning
         reversed)
     [!] Steps parameter of walk changed to Distance
     [*] Correct handling of speed vectors other than [1,1] in walk scenelet
     [*] Correct speed even if general framerate is different from map engine
         framerate
     [*] Camera and fades reset correctly even if forks are running when main
         queue finishes
     [*] Correct handling of zero duration in fadeTo and panTo scenelets
     [+] walk instructions can specify speed
     [+] Built-in scenelets focusOn, playSound, kill
   
   1.1 - Tuesday, September 2, 2008
     [!] Renamed methods: flush became synchronize, queue became enqueue
     [+] Scenelets can specify a function to be called when first started
     [+] Built-in scenelets pause, walk, fadeTo, face, panTo, follow
     [+] Player input is disabled during cutscene execution
     [+] Camera state is saved before executing a cutscene and restored
         afterwards
   
   1.0 - Monday, September 1, 2008
     First release!
*/

if (!Scenario.Initialized) {
	Scenario.FadeMask = CreateColor(0,0,0,0);
	Scenario.FocusStack = [];
	Scenario.FocusThread = 0;
	Scenario.NextThreadID = 1;
	Scenario.Running = false;
	Scenario.Threads = [];
	Scenario.Initialized = true;
}

function Scenario ()
{
}

Scenario.addThread = function (State,Updater,Renderer,Priority,InputHandler)
{
	if (Priority == null) Priority = 0;
	var ThreadData = {};
	ThreadData.ID = Scenario.NextThreadID;
	ThreadData.State = State;
	ThreadData.Priority = Priority;
	ThreadData.Updater = Updater;
	ThreadData.Renderer = Renderer;
	ThreadData.InputHandler = InputHandler;
	Scenario.Threads.push(ThreadData);
	var Comparer = function(A,B) {
		if (A.Priority < B.Priority) return -1;
		if (A.Priority > B.Priority) return 1;
		return 0;
	};
	Scenario.Threads.sort(Comparer);
	if (InputHandler != null) {
		Scenario.FocusStack.push(Scenario.FocusThread);
		Scenario.FocusThread = ThreadData.ID;
	}
	++Scenario.NextThreadID;
	return ThreadData.ID;
};

Scenario.findThread = function (ID)
{
	if (ID == 0) return false;
	for (var i = 0; i < Scenario.Threads.length; ++i) {
		if (ID == Scenario.Threads[i].ID) return true;
	}
	return false;
};

Scenario.killThread = function (ID)
{
	for (var i = 0; i < Scenario.Threads.length; ++i) {
		if (ID == Scenario.Threads[i].ID) {
			Scenario.Threads.splice(i,1);
			--i; continue;
		}
	}
};

Scenario.render = function ()
{
	for (var iThread = 0; iThread < Scenario.Threads.length; ++iThread) {
		var Renderer = Scenario.Threads[iThread].Renderer;
		if (Renderer != null) Renderer(Scenario.Threads[iThread].State);
	}
};

Scenario.update = function ()
{
	for (var iThread = 0; iThread < Scenario.Threads.length; ++iThread) {
		var ID = Scenario.Threads[iThread].ID;
		var Updater = Scenario.Threads[iThread].Updater;
		var InputHandler = Scenario.Threads[iThread].InputHandler;
		var State = Scenario.Threads[iThread].State;
		if (Updater == null) continue;
		if (!Updater(State)) {
			if (Scenario.FocusThread == ID) Scenario.FocusThread = Scenario.FocusStack.pop();
			Scenario.Threads.splice(iThread,1);
			--iThread; continue;
		}
		if (Scenario.FocusThread == ID) InputHandler(State);
	}
};

CutScene.Update = function (State)
{
	for (var iFork = 0; iFork < State.ForkThreads.length; ++iFork) {
		if (!Scenario.findThread(State.ForkThreads[iFork])) {
			State.ForkThreads.splice(iFork,1);
			--iFork; continue;
		}
	}
	if (Scenario.findThread(State.CommandThread)) return true;
	if (State.Instructions.length == 0 && State.ForkThreads.length == 0) return false;
	if (State.Instructions.length > 0) {
		var Instruction = State.Instructions.shift();
		var StateObject = (Instruction.State == null) ? null : Instruction.State;
		if (Instruction.start != null) {
			var Started = Instruction.start(StateObject);
			if (!Started) return true;
		}
		var Priority = Instruction.Priority == null ? 0 : Math.max(0,Math.floor(Instruction.Priority));
		if (Instruction.update == null) return true;
		State.CommandThread = Scenario.addThread(StateObject,Instruction.update,Instruction.render,Priority,Instruction.handleInput);
	}
	return true;
};

function CutScene ()
{
	this.ForkArrays = [];
	this.CurrentForkArray = [];
	this.CurrentQueue = [];
	this.Queues = [];
}

CutScene.prototype.beginFork = function ()
{
	this.ForkArrays.push(this.CurrentForkArray);
	this.CurrentForkArray = [];
	this.Queues.push(this.CurrentQueue);
	this.CurrentQueue = [];
};

CutScene.prototype.endFork = function ()
{
	var ThreadArray = this.CurrentForkArray;
	this.CurrentForkArray = this.ForkArrays.pop();
	var ParentThreadArray = this.CurrentForkArray;
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.ParentThreadArray = ParentThreadArray;
	Instruction.State.ThreadArray = ThreadArray;
	Instruction.State.Queue = this.CurrentQueue;
	Instruction.update = function(State) {
		var ForkState = {};
		ForkState.Instructions = State.Queue;
		ForkState.CommandThread = 0;
		ForkState.ForkThreads = State.ThreadArray;
		var Thread = Scenario.addThread(ForkState,CutScene.Update,null,-1);
		State.ParentThreadArray.push(Thread);
		return false;
	};
	this.CurrentQueue = this.Queues.pop();
	this.enqueue(Instruction);
};

CutScene.prototype.enqueue = function (Instruction)
{
	this.CurrentQueue.push(Instruction);
};

CutScene.prototype.fadeTo = function (Color,Duration)
{
	if (Duration == null) Duration = 250;
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.Color = Color;
	Instruction.State.Duration = Duration;
	Instruction.start = function(State) {
		if (State.Duration <= 0) {
			Scenario.FadeMask = Color;
			return false;
		}
		if (State.Color.red == Scenario.FadeMask.red
			&& State.Color.green == Scenario.FadeMask.green
			&& State.Color.blue == Scenario.FadeMask.blue
			&& State.Color.alpha == Scenario.FadeMask.alpha)
		{
			return false;
		}
		var Multiplier = 1000 / State.Duration;
		State.RInterval = Multiplier * ((State.Color.red - Scenario.FadeMask.red) / Scenario.FrameRate);
		State.GInterval = Multiplier * ((State.Color.green - Scenario.FadeMask.green) / Scenario.FrameRate);
		State.BInterval = Multiplier * ((State.Color.blue - Scenario.FadeMask.blue) / Scenario.FrameRate);
		State.AInterval = Multiplier * ((State.Color.alpha - Scenario.FadeMask.alpha) / Scenario.FrameRate);
		return true;
	};
	Instruction.update = function(State) {
		var Red = Scenario.FadeMask.red;
		var Green = Scenario.FadeMask.green;
		var Blue = Scenario.FadeMask.blue;
		var Alpha = Scenario.FadeMask.alpha;
		Red += State.RInterval;
		if (Red > State.Color.red && State.RInterval > 0) Red = State.Color.red;
			else if (Red < State.Color.red && State.RInterval < 0) Red = State.Color.red;
		Green += State.GInterval;
		if (Green > State.Color.green && State.GInterval > 0) Green = State.Color.green;
			else if (Green < State.Color.green && State.GInterval < 0) Green = State.Color.green;
		Blue += State.BInterval;
		if (Blue > State.Color.blue && State.BInterval > 0) Blue = State.Color.blue;
			else if (Blue < State.Color.blue && State.BInterval < 0) Blue = State.Color.blue;
		Alpha += State.AInterval;
		if (Alpha > State.Color.alpha && State.AInterval > 0) Alpha = State.Color.alpha;
			else if (Alpha < State.Color.alpha && State.AInterval < 0) Alpha = State.Color.alpha;
		Scenario.FadeMask = CreateColor(Red,Green,Blue,Alpha);
		return State.Color.red != Scenario.FadeMask.red
			|| State.Color.green != Scenario.FadeMask.green
			|| State.Color.blue != Scenario.FadeMask.blue
			|| State.Color.alpha != Scenario.FadeMask.alpha;
	};
	this.enqueue(Instruction);
};

CutScene.prototype.face = function (Person,Direction)
{
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.Person = Person;
	Instruction.State.Direction = Direction;
	Instruction.start = function(State) {
		var FaceCommand;
		switch (State.Direction.toLowerCase()) {
			case "n": FaceCommand = COMMAND_FACE_NORTH; break;
			case "s": FaceCommand = COMMAND_FACE_SOUTH; break;
			case "e": FaceCommand = COMMAND_FACE_EAST; break;
			case "w": FaceCommand = COMMAND_FACE_WEST; break;
			default: FaceCommand = COMMAND_WAIT;
		}
		QueuePersonCommand(State.Person,FaceCommand,true);
	};
	this.enqueue(Instruction);
};

CutScene.prototype.focusOn = function (Person,Duration)
{
	if (Duration == null) Duration = 250;
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.Person = Person;
	Instruction.State.Duration = Duration;
	Instruction.start = function(State) {
		DetachCamera();
		State.XTarget = GetPersonX(State.Person);
		State.YTarget = GetPersonY(State.Person);
		State.X = State.Duration != 0 ? GetCameraX() : XTarget;
		State.Y = State.Duration != 0 ? GetCameraY() : YTarget;
		if (State.XTarget == State.X && State.YTarget == State.Y) return false;
		var Multiplier = 1000 / State.Duration;
		State.XInterval = Multiplier * ((State.XTarget - State.X) / Scenario.FrameRate);
		State.YInterval = Multiplier * ((State.YTarget - State.Y) / Scenario.FrameRate);
		return true;
	};
	Instruction.update = function(State) {
		State.X += State.XInterval;
		if (State.X > State.XTarget && State.XInterval > 0) State.X = State.XTarget;
			else if (State.X < State.XTarget && State.XInterval < 0) State.X = State.XTarget;
		State.Y += State.YInterval;
		if (State.Y > State.YTarget && State.YInterval > 0) State.Y = State.YTarget;
			else if (State.Y < State.YTarget && State.YInterval < 0) State.Y = State.YTarget;
		SetCameraX(State.X); SetCameraY(State.Y);
		return State.X != State.XTarget || State.Y != State.YTarget;
	};
	this.enqueue(Instruction);
};

CutScene.prototype.follow = function (Person)
{
	this.focusOn(Person);
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.Person = Person;
	Instruction.start = function(State) {
		AttachCamera(State.Person);
	};
	this.enqueue(Instruction);
};

CutScene.prototype.hide = function (Person)
{
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.Person = Person;
	Instruction.start = function(State) {
		SetPersonVisible(State.Person,false);
		IgnorePersonObstructions(State.Person,true);
	};
	this.enqueue(Instruction);
}

CutScene.prototype.kill = function (Person)
{
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.Person = Person;
	Instruction.start = function(State) {
		DestroyPerson(Person);
	};
	this.enqueue(Instruction);
};

CutScene.prototype.panTo = function (X,Y,Duration)
{
	if (Duration == null) Duration = 250;
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.XTarget = X;
	Instruction.State.YTarget = Y;
	Instruction.State.Duration = Duration;
	Instruction.start = function(State) {
		DetachCamera();
		State.X = State.Duration != 0 ? GetCameraX() : State.XTarget;
		State.Y = State.Duration != 0 ? GetCameraY() : State.YTarget;
		if (State.XTarget == State.X && State.YTarget == State.Y) return false;
		var Multiplier = 1000 / State.Duration;
		State.XInterval = Multiplier * ((State.XTarget - State.X) / Scenario.FrameRate);
		State.YInterval = Multiplier * ((State.YTarget - State.Y) / Scenario.FrameRate);
		return true;
	};
	Instruction.update = function(State) {
		State.X += State.XInterval;
		if (State.X > State.XTarget && State.XInterval > 0) State.X = State.XTarget;
			else if (State.X < State.XTarget && State.XInterval < 0) State.X = State.XTarget;
		State.Y += State.YInterval;
		if (State.Y > State.YTarget && State.YInterval > 0) State.Y = State.YTarget;
			else if (State.Y < State.YTarget && State.YInterval < 0) State.Y = State.YTarget;
		SetCameraX(State.X); SetCameraY(State.Y);
		return State.X != State.XTarget || State.Y != State.YTarget;
	};
	this.enqueue(Instruction);
};

CutScene.prototype.pause = function (Duration)
{
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.Duration = Duration;
	Instruction.start = function(State) {
		State.EndTime = State.Duration + GetTime();
		return true;
	};
	Instruction.update = function(State) {
		return GetTime() < State.EndTime;
	};
	this.enqueue(Instruction);
};

CutScene.prototype.playSound = function (File)
{
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.File = File;
	Instruction.start = function(State) {
		State.Sound = LoadSound(State.File);
		State.Sound.play(false);
		return true;
	}
	Instruction.update = function(State) {
		return State.Sound.isPlaying();
	}
	this.enqueue(Instruction);
};

CutScene.prototype.show = function (Person)
{
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.Person = Person;
	Instruction.start = function(State) {
		SetPersonVisible(State.Person,true);
		IgnorePersonObstructions(State.Person,false);
	};
	this.enqueue(Instruction);
}

CutScene.prototype.synchronize = function ()
{
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.Threads = this.CurrentForkArray;
	Instruction.update = function(State) {
		return State.Threads.length != 0;
	};
	this.enqueue(Instruction);
};

CutScene.prototype.walk = function (Person,Direction,Distance,Speed,FaceFirst)
{
	if (FaceFirst == null) FaceFirst = true;
	if (!isNaN(Speed)) SpeedVector = [Speed,Speed];
		else SpeedVector = Speed;
	var Instruction = {};
	Instruction.State = {};
	Instruction.State.Direction = Direction;
	Instruction.State.Distance = Distance;
	Instruction.State.FaceFirst = FaceFirst;
	Instruction.State.Person = Person;
	Instruction.State.Speed = SpeedVector;
	Instruction.start = function(State) {
		State.OldSpeed = [GetPersonSpeedX(State.Person),GetPersonSpeedY(State.Person)];
		if (State.Speed != null) SetPersonSpeedXY(State.Person,State.Speed[0],State.Speed[1]);
			else State.Speed = State.OldSpeed;
		var XMovement;
		var YMovement;
		var FaceCommand;
		var StepCount;
		switch (State.Direction) {
			case "n":
				FaceCommand = COMMAND_FACE_NORTH;
				XMovement = COMMAND_WAIT;
				YMovement = COMMAND_MOVE_NORTH;
				StepCount = State.Distance / State.Speed[1];
				break;
			case "s":
				FaceCommand = COMMAND_FACE_SOUTH;
				XMovement = COMMAND_WAIT;
				YMovement = COMMAND_MOVE_SOUTH;
				StepCount = State.Distance / State.Speed[1];
				break;
			case "w":
				FaceCommand = COMMAND_FACE_WEST;
				XMovement = COMMAND_MOVE_WEST;
				YMovement = COMMAND_WAIT;
				StepCount = State.Distance / State.Speed[0];
				break;
			case "e":
				FaceCommand = COMMAND_FACE_EAST;
				XMovement = COMMAND_MOVE_EAST;
				YMovement = COMMAND_WAIT;
				StepCount = State.Distance / State.Speed[0];
				break;
			default:
				FaceCommand = COMMAND_WAIT;
				XMovement = COMMAND_WAIT;
				YMovement = COMMAND_WAIT;
				StepCount = 0;
		}
		if (FaceFirst) QueuePersonCommand(State.Person,FaceCommand,true);
		for (iStep = 0; iStep < StepCount; ++iStep) {
			QueuePersonCommand(State.Person,XMovement,true);
			QueuePersonCommand(State.Person,YMovement,true);
			QueuePersonCommand(State.Person,COMMAND_WAIT,false);
		}
		return true;
	};
	Instruction.update = function(State) {
		if (IsCommandQueueEmpty(State.Person)) {
			SetPersonSpeedXY(State.Person,State.OldSpeed[0],State.OldSpeed[1]);
			return false;
		}
		return true;
	};
	this.enqueue(Instruction);
};

CutScene.prototype.execute = function ()
{
	if (!IsMapEngineRunning()) Abort("Scenario Cutscene System:\nCan't execute cutscene without a running map engine")
	if (Scenario.Running) return;
	this.synchronize();
	var OldPC = IsInputAttached() ? GetInputPerson() : null;
	var OldCameraTarget;
	var OldCameraX;
	var OldCameraY;
	if (!IsCameraAttached()) {
		OldCameraX = GetCameraX();
		OldCameraY = GetCameraY();
		this.beginFork();
		  this.panTo(OldCameraX,OldCameraY);
		this.endFork();
	}
	else {
		OldCameraTarget = GetCameraPerson();
		this.beginFork();
			this.follow(OldCameraTarget);
		this.endFork();
	}
	this.beginFork();
		this.fadeTo(CreateColor(0,0,0,0));
	this.endFork();
	DetachInput();
	var FadeRenderer = function() {
		ApplyColorMask(Scenario.FadeMask);
	}
	var FadeThread = Scenario.addThread(null,null,FadeRenderer,999.5);
	var State = {};
	State.CommandThread = 0;
	State.Instructions = this.CurrentQueue;
	State.ForkThreads = this.CurrentForkArray;
	var Thread = Scenario.addThread(State,CutScene.Update,null,-1);
	Scenario.Running = true;
	Scenario.FrameRate = GetMapEngineFrameRate();
	var OldFrameRate = GetFrameRate();
	SetFrameRate(0);
	var NextFrameTime = GetTime() + (1000 / Scenario.FrameRate);
	while (Scenario.findThread(Thread)) {
		RenderMap();
		Scenario.render();
		FlipScreen();
		while (NextFrameTime <= GetTime()) {
			NextFrameTime += (1000 / Scenario.FrameRate);
			UpdateMapEngine();
			Scenario.update();
		}
	}
	SetFrameRate(OldFrameRate);
	if (OldPC != null) AttachInput(OldPC);
	if (OldCameraTarget != null) {
		AttachCamera(OldCameraTarget);
	}
	else {
		DetachCamera();
		SetCameraX(OldCameraX);
		SetCameraY(OldCameraY);
	}
	Scenario.killThread(FadeThread);
	Scenario.Running = false;
};
