Downloads containing SaLLiB-v2.0.asc

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Devan's Revenge (Intro)Featured Download sAlAmAnDeR Single player 9.6 Download file

File preview

//SaLLiB v2.0
//By sAlAmAnDeR
#pragma require "sepalwiz-1.1.asc"
#include "sepalwiz-1.1.asc"

//An unused object constant, to mark custom stuff
OBJECT::Object NIL = OBJECT::AMBIENTSOUND;

//BEHAVIOR::INACTIVE actually calls the objects behavior function, but BEHAVIOR::BEES actually does nothing.
jjBEHAVIOR DO_NOTHING = BEHAVIOR::BEES;

float PI = 3.1416;
float HALF_PI = 0.5*PI;
float ONE_AND_HALF_PI = 1.5*PI;
float TWO_PI = 2*PI;
float E = 2.71828;

float distance(float xStart, float yStart, float xEnd, float yEnd) {return sqrt((xEnd - xStart)*(xEnd - xStart) + (yEnd - yStart)*(yEnd - yStart));}

float RAND_MAX = 4294967295;

float random(float a, float b) {
    float random =  jjRandom() / RAND_MAX;
    float diff = b - a;
    float r = random * diff;
    return a + r;
}

funcdef void OBJECTHITHOOK(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force);
funcdef void CANVASHOOKVOID(jjPLAYER@ player, jjCANVAS@ canvas);

OBJECTHITHOOK@  onObjectHitHook		  = null;
CANVASHOOKVOID@ onDrawAmmoHook	 	  = null;
CANVASHOOKVOID@ onDrawHealthHook 	  = null;
CANVASHOOKVOID@ onDrawLivesHook 	  = null;
CANVASHOOKVOID@ onDrawScoreHook		  = null;
CANVASHOOKVOID@ onDrawPlayerTimerHook = null;

string chr(uint8 value) {
  string str="\0";
  str[0]=value;
  return str;
}

string stripPipes(string str) {
	string result = "";
	for(uint i = 0; i < str.length(); i++)
		if(str[i] != 124)
			result += chr(str[i]);
	return result;
}

namespace COORDINATES {
	enum CoordinateType {
		LAYER = 0,
		SCREEN = 1
	}
}

namespace COMMON {
	interface IFocalPoint {
		float get_xPos() const;
		float get_yPos() const;
		bool  get_isActive() const;
	}

	class Position : IFocalPoint {
		float x, y;
		Position(float xPos, float yPos) {this.x = xPos;
										  this.y = yPos;}
										  
		float get_xPos() const 			 {return this.x;}
		float get_yPos() const 			 {return this.y;}
		
		void  set_xPos(float xPos) 		 {this.x=xPos;}
		void  set_yPos(float yPos) 		 {this.y=yPos;}
		
		bool  get_isActive() const		 {return true;}
	}

	//TODO: Maybe set a variable when gameTicks == 0 to indicate timers are safe to use?
	funcdef int TIME_UNIT();
	
	int jjGameTicks() { return ::jjGameTicks; }
	int jjRenderFrame() { return ::jjRenderFrame; }
	
	class Timer {
		int inter, startTime, end;
		bool running;
		TIME_UNIT@ timeUnit = COMMON::jjGameTicks;
		
		Timer() {
			this.running = false;
		}
		
		Timer start(int interval) {
			if(interval < 0)
				return start();
			this.inter = interval;
			this.startTime = timeUnit();
			this.end = this.startTime + this.inter;
			this.running = true;
			return this;
		}
		
		Timer start() {
			this.startTime = timeUnit();
			this.running = true;
			this.end = -1;
			this.inter = -1;
			return this;
		}
		
		Timer stop() {
			this.running = false;
			return this;
		}
		
		bool isFinished() const {
			if(!this.running) return false;
			if(this.end == -1) return false;
			return timeUnit() >= this.end;
		}
		
		int elapsedTime() const {
			return timeUnit()-this.startTime;
		}
		
		int endTime() const {
			return end;
		}
		
		int remainingTime() const {
			if(this.interval() == -1) return -1;
			return this.interval() - this.elapsedTime();
		}
		
		int interval() const {
			return this.inter;
		}
		
		Timer reset() {
			start(this.inter);
			return this;
		}
		
		bool isStarted() const {
			return this.running;
		}
		
		Timer useGameTicks() {
			@timeUnit = @COMMON::jjGameTicks;
			return this;
		}
		
		Timer useRenderFrames() {
			@timeUnit = @COMMON::jjRenderFrame;
			return this;
		}
	}
}

namespace CUSTOM {
	bool SHOW_WARNINGS = true;
	bool SALLIB_DEBUG = true;
	
	void CustomObjectBehavior(jjOBJ@ jjo) {
		Object@ co = objects[jjo.objectID];
		if(co != jjo) {//jjo is active or the behavior would not be called
			if(SALLIB_DEBUG) jjDebug("SaLLiB DEBUG: Object behavior changed.");
			return;
		}
		
		co.age = co.age + 1;
		co.behavior();
		if(!co.isActive) return;
		co.move();
		co.rotate();
		co.color();
		co.scale();
		co.patrol();
		co.warp();
		co.updateDefaultBehavior();
	}
	
	int theoreticalMaxObjects = jjGameMode == GAME::SP ? 512:2560;
	array<CUSTOM::Object@> objects(jjObjectMax == 0 ? theoreticalMaxObjects : jjObjectMax);
	int nextID = 1;
	
	interface ICustomObject : COMMON::IFocalPoint,MIXIN::IMovable,MIXIN::IScalable,MIXIN::IRotatable,MIXIN::IColorable,MIXIN::IPatrollable,MIXIN::IWarpable {
		void behavior();
		void onObjectHit(jjOBJ@ bullet, jjPLAYER@ player, int force);
		bool get_isActive() const;
		
		jjBEHAVIOR get_standardBehavior();
		
		int get_age() const;
		void set_age(int age);
		
		//Sprite accessors/mutators
		int 			get_spriteAngle() const;
		float 			get_spriteScaleX() const;
		float 			get_spriteScaleY() const;
		SPRITE::Mode 	get_spriteMode() const;
		uint8 			get_spriteParam() const;
		uint8			get_spriteLayerZ() const;
		uint8			get_spriteLayerXY()	const;
		int8 			get_spritePlayerID() const;
		bool 			get_spriteBob()	const;
		ANIM::Set		get_set() const;
		int				get_animation()	const;
		int				get_startFrame() const;
		int				get_numFrames()	const;
		COMMON::Timer@  get_animationTimer();
		int				get_ticksPerFrame()	const;
		bool			get_cycleAnimation() const;
		bool			get_invisible()	const;
		int				get_frame()	const;
		
		void 			set_spriteAngle(int angle);
		void 			set_spriteScaleX(float scaleX);
		void 			set_spriteScaleY(float scaleY);
		void		 	set_spriteMode(SPRITE::Mode mode);
		void 			set_spriteParam(uint8 param);
		void			set_spriteLayerZ(uint8 layerZ);
		void			set_spriteLayerXY(uint8 layerXY);
		void 			set_spritePlayerID(int8 playerID);
		void 			set_spriteBob(bool bob);
		void			set_set(ANIM::Set set);
		void			set_animation(int animation);
		void			set_startFrame(int startFrame);
		void			set_numFrames(int numFrames);
		void			set_ticksPerFrame(int ticksPerFrame);
		void			set_cycleAnimation(bool cycleAnimation);
		void			set_invisible(bool invisible);
		
		void playAnimation(ANIM::Set set, int animation, int startFrame, int numFrames, int ticksPerFrame = 7, bool cycle=true);
		
		bool isPlayingAnimation();
		
		void stopAnimation();
		
		void freezeFrame(ANIM::Set, int animation, int frame);
		
		//Custom object methods
		float distancePixels(COMMON::IFocalPoint@ focus);
		float distanceTiles(COMMON::IFocalPoint@ focus);
	
		bool opEquals(jjOBJ@ jjObject);
		bool opEquals(CUSTOM::Object object);
		
		void updateDefaultBehavior();
		//jjOBJ accessors 
		int get_animSpeed() const;
		HANDLING::Bullet get_bulletHandling() const;
		bool get_causesRicochet() const;
		int get_counter() const;
		uint8 get_counterEnd() const;
		int get_creator() const;
		int get_creatorID() const;
		CREATOR::Type get_creatorType() const;
		int16 get_curAnim() const;
		uint get_curFrame() const;
		bool get_deactivates() const;
		int8 get_direction() const;
		uint8 get_doesHurt() const;
		int get_energy() const;
		uint8 get_eventID() const;
		int8 get_frameID() const;
		uint8 get_freeze() const;
		bool get_isBlastable() const;
		bool get_isFreezable() const;
		bool get_isTarget() const;
		uint8 get_justHit() const;
		int16 get_killAnim() const;
		int8 get_light() const;
		uint8 get_lightType() const;
		int get_oldState() const;
		HANDLING::Player get_playerHandling() const;
		uint16 get_points() const;
		int8 get_noHit() const;
		uint16 get_objectID() const;
		uint8 get_objType() const;
		bool get_scriptedCollisions() const;
		int get_special() const;
		int get_state() const;
		bool get_triggersTNT() const;
		int get_var(int index) const;
		float get_xAcc() const;
		float get_xOrg() const;
		float get_xPos() const;
		float get_xSpeed() const;
		float get_yAcc() const;
		float get_yPos() const;
		float get_yOrg() const;
		float get_ySpeed() const;
		
		
		void set_animSpeed(int animSpeed);
		void set_bulletHandling(HANDLING::Bullet bulletHandling);
		void set_causesRicochet(bool causesRicochet);
		void set_counter(int counter);
		void set_counterEnd(uint8 counterEnd);
		void set_creator(int creator);
		void set_creatorID(int creatorID);
		void set_creatorType(CREATOR::Type creatorType);
		void set_curAnim(int16 curAnim);
		void set_curFrame(uint curFrame);
		void set_deactivates(bool deactivates);
		void set_direction(int8 direction);
		void set_doesHurt(uint8 doesHurt);
		void set_energy(int energy);
		void set_eventID(uint8 eventID);
		void set_frameID(int8 frameID);
		void set_freeze(uint8 freeze);
		void set_isBlastable(bool isBlastable);
		void set_isFreezable(bool isFreezable);
		void set_isTarget(bool isTarget);
		void set_justHit(uint8 justHit);
		void set_killAnim(int16 killAnim);
		void set_light(int8 light);
		void set_lightType(uint8 lightType);
		void set_oldState(STATE::State state);
		void set_playerHandling(HANDLING::Player playerHandling);
		void set_points(uint16 points);
		void set_noHit(int8 noHit);
		void set_objType(uint8 objType);
		void set_scriptedCollisions(bool scriptedCollisions);
		void set_special(int special);
		void set_state(int state);
		void set_triggersTNT(bool triggersTNT);
		void set_var(int index, int value);
		void set_xAcc(float xAcc);
		void set_xOrg(float xOrg);
		void set_xPos(float xPos);
		void set_xSpeed(float xSpeed);
		void set_ySpeed(float ySpeed);
		void set_yAcc(float yAcc);
		void set_yPos(float yPos);
		void set_yOrg(float yOrg);
		
		
		void clearPlatform() const;
		void grantPickup(jjPLAYER@ player, int frequency) const;
		void particlePixelExplosion(int style) const;
		void pathMovement() const;
		void putOnGround(bool precise = false) const;
		void delete();
		
		int 	beSolid() const;
		int16 	determineCurAnim(uint8 setID, uint8 animation, bool change = true) const;
		int16 	determineCurAnim(ANIM::Set setID, uint8 animation, bool change = true) const;
		uint 	determineCurFrame(bool change = true) const;
		int 	findNearestPlayer(int maxDistance) const;
		int 	findNearestPlayer(int maxDistance, int &out foundDistance) const;
		int 	fireBullet(OBJECT::Object eventID) const;
		int 	unfreeze(int style) const;
		bool 	ricochet() const;
	
		
		void deactivate();
		void behave(BEHAVIOR::Behavior behavior, bool draw = true, bool useCustomSpriteProperties = true) const;
		void behave(jjVOIDFUNCOBJ@ behavior, bool draw = true, bool useCustomSpriteProperties = true) const;
		void behave(jjBEHAVIOR behavior, bool draw = true, bool useCustomSpriteProperties = true) const;
		int draw(bool useCustomSpriteProperties = true) const;
		void printPos();
	}

	class Object : ICustomObject, MIXIN::Movable, MIXIN::Scalable, 
				   MIXIN::Rotatable, MIXIN::Colorable, MIXIN::Patrollable,
				   MIXIN::Warpable {
		//Object Properties
		jjOBJ@ 				jjObject = null;
		jjBEHAVIOR 			defaultBehavior;
		int 				customAge = 0, cid;
		bool isNil = false;
		
		//Sprite properties
		int angle = 0;
		float scaleX = 1, scaleY = 1;
		SPRITE::Mode mode = SPRITE::NORMAL;
		uint8 param = 0, layerZ = 4, layerXY = 4;
		int8 playerID = -1;
		bool bob = false;
		COMMON::Timer bobTimer(), anmTimer();
		ANIM::Set st;
		int anim, fram, numFrams, startFram, ticksPerFram;
		bool cycleAnim = false, inv = false;
		INTERPOLATION::Sinusoidal@ bobInterpolator;
		
		Object(OBJECT::Object objectTemplate, float xPos, float yPos) {
			@this.jjObject = @jjObjects[jjAddObject(objectTemplate,xPos,yPos,0,CREATOR::OBJECT)];
			if(objectTemplate == NIL) {
				isNil = true;
				this.jjObject.playerHandling = HANDLING::PARTICLE;
				this.jjObject.bulletHandling = HANDLING::IGNOREBULLET;
				this.defaultBehavior = DO_NOTHING;
			} else 
				initDefaultBehavior();
			initialize();
		}
		
		Object(jjOBJ@ jjObject) {
			@this.jjObject = jjObject;
			initDefaultBehavior();
			initialize();
		}
		
		void initDefaultBehavior() {
			if(this.jjObject.behavior == BEHAVIOR::DEFAULT) {
				if(SALLIB_DEBUG) jjDebug("SaLLiB DEBUG: jjObject passed to custom object uses BEHAVIOR::DEFAULT");
				this.defaultBehavior = BEHAVIOR::BULLET;
			} else if(this.jjObject.behavior == CustomBulletBehavior1) this.defaultBehavior = BEHAVIOR::BULLET;
			else if(this.jjObject.behavior == CustomBulletBehavior2) this.defaultBehavior = BEHAVIOR::BOUNCERBULLET;
			else if(this.jjObject.behavior == CustomBulletBehavior3) this.defaultBehavior = BEHAVIOR::BULLET;
			else if(this.jjObject.behavior == CustomBulletBehavior4) this.defaultBehavior = BEHAVIOR::SEEKERBULLET;
			else if(this.jjObject.behavior == CustomBulletBehavior5) this.defaultBehavior = BEHAVIOR::RFBULLET;
			else if(this.jjObject.behavior == CustomBulletBehavior6) this.defaultBehavior = BEHAVIOR::TOASTERBULLET;
			else if(this.jjObject.behavior == CustomBulletBehavior7) this.defaultBehavior = BEHAVIOR::TNT;
			else if(this.jjObject.behavior == CustomBulletBehavior8) this.defaultBehavior = BEHAVIOR::PEPPERBULLET;
			else if(this.jjObject.behavior == CustomBulletBehavior9) this.defaultBehavior = BEHAVIOR::ELECTROBULLET;
			else this.defaultBehavior = this.jjObject.behavior;
		}
		
		void initialize() {
			this.jjObject.behavior = CustomObjectBehavior;
			
			@CUSTOM::objects[jjObject.objectID] = @this;
			this.jjObject.age = nextID;
			this.cid = nextID++;

			@this.bobInterpolator = INTERPOLATION::Sinusoidal(2,3,jjRandom()%100,65);
			this.bobTimer.start();
		}
		
		//Methods made to be overridden
		void behavior() {
			behave(this.defaultBehavior);
		}
		
		void onObjectHit(jjOBJ@ bullet, jjPLAYER@ player, int force)  	{}
		
		bool get_isActive() const {return this.jjObject.isActive && this.jjObject.age == this.cid;}
		
		//Handling jjObject changing out from under you
		bool objectChanged(jjOBJ@ jjo) {
			if(!isActive) { 
				if(jjo.behavior == CustomObjectBehavior)
					jjo.behavior = DO_NOTHING;
				return true;
			}
			return false;
		}
		
		jjBEHAVIOR get_standardBehavior() { if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_standardBehavior()");}}return defaultBehavior; }
		
		int get_age() const 	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_age()");}}				return this.customAge;}
		void set_age(int age) 	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_age()");}return;}	this.customAge=age;}
		
		//Sprite accessors/mutators
		int 			get_spriteAngle() const		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_spriteAngle()");}}		return angle;}
		//float			get_spriteAngleRadians()	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_spriteAngleRadians()");}}	return angle;}
		//float			get_spriteAngleDegrees()	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_spriteAngleDegrees()");}}	return angle;}
		float 			get_spriteScaleX() const	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_spriteScaleX()");}}		return scaleX;}
		float 			get_spriteScaleY() const	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_spriteScaleY()");}}		return scaleY;}
		SPRITE::Mode 	get_spriteMode() const		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_spriteMode()");}}			return mode;}
		uint8 			get_spriteParam() const		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_spriteParam()");}}		return param;}
		uint8			get_spriteLayerZ() const	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_spriteLayerZ()");}}		return layerZ;}
		uint8			get_spriteLayerXY()	const 	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_spriteLayerXY()");}}		return layerXY;}
		int8 			get_spritePlayerID() const	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_spritePlayerID()");}}		return playerID;}
		bool 			get_spriteBob()	const 		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_spriteBob()");}}			return bob;}
		ANIM::Set		get_set() const 			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_set()");}}				return st;}
		int				get_animation()	const		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_animation()");}}			return anim;}
		int				get_startFrame() const		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_startFrame()");}}			return startFram;}
		int				get_numFrames()	const		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_numFrames()");}}			return numFrams;}
		COMMON::Timer@  get_animationTimer()		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_animationTimer()");}}		return anmTimer;}
		int				get_ticksPerFrame()	const	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_ticksPerFrame()");}}		return ticksPerFram;}
		bool			get_cycleAnimation() const	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_cycleAnimation()");}}		return cycleAnim;}
		bool			get_invisible()	const		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_invisible()");}}			return inv;}
		
		int				get_frame()	const			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_frame()");}}
													 return startFrame + (anmTimer.elapsedTime()/ticksPerFrame) % numFrames;}
		
		
		void 			set_spriteAngle(int angle) 				{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_spriteAngle()");}return;}		this.angle = angle;}
		void 			set_spriteScaleX(float scaleX)			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_spriteScaleX()");}return;}	this.scaleX = scaleX;}
		void 			set_spriteScaleY(float scaleY) 			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_spriteScaleY()");}return;}	this.scaleY = scaleY;}
		void		 	set_spriteMode(SPRITE::Mode mode)		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_spriteMode()");}return;}		this.mode = mode;}
		void 			set_spriteParam(uint8 param)			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_spriteParam()");}return;}		this.param = param;}
		void			set_spriteLayerZ(uint8 layerZ)			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_spriteLayerZ()");}return;}	this.layerZ = layerZ;}
		void			set_spriteLayerXY(uint8 layerXY)		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_spriteLayerXY()");}return;}	this.layerXY = layerXY;}
		void 			set_spritePlayerID(int8 playerID)		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_spritePlayerID()");}return;}	this.playerID = playerID;}	
		void 			set_spriteBob(bool bob)					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_spriteBob()");}return;}		this.bob = bob;}
		void			set_set(ANIM::Set set)					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_set()");}return;}				this.st = set;}
		void			set_animation(int animation)			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_animation()");}return;}		this.anim = animation;}
		void			set_startFrame(int startFrame)			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_startFame()");}return;}		this.startFram = startFrame;}
		void			set_numFrames(int numFrames)			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_numFrames()");}return;}		this.numFrams = numFrames;}
		void			set_ticksPerFrame(int ticksPerFrame)	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_ticksPerFrame()");}return;}	this.ticksPerFram = ticksPerFrame;}
		void			set_cycleAnimation(bool cycleAnimation)	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_cycleAnimation()");}return;}	this.cycleAnim = cycleAnimation;}
		void			set_invisible(bool invisible)			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_invisible()");}return;}		this.inv = invisible;}
		
		void playAnimation(ANIM::Set set, int animation, int startFrame, int numFrames, int ticksPerFrame = 7, bool cycle=true) {
			if(this.anmTimer.isFinished() || set != st || animation != anim || startFrame != startFram || numFrames != numFrams || ticksPerFrame != ticksPerFram) {
				this.st = set;
				this.anim = animation;
				this.startFram = startFrame;
				this.numFrams = numFrames;
				this.ticksPerFram = ticksPerFrame;
				this.anmTimer.start(numFrams*ticksPerFram);
			}
			this.cycleAnim = cycle;
		}
		
		bool isPlayingAnimation() {
			return (this.anmTimer.isStarted() && !this.anmTimer.isFinished()) || cycleAnim;
		}
		
		void stopAnimation() {
			this.anmTimer.stop();
		}
		
		void freezeFrame(ANIM::Set, int animation, int frame) {
			playAnimation(set,animation,frame,1,1,true);
		}
		
		//Custom object methods
		float distancePixels(COMMON::IFocalPoint@ focus) {return distance(this.xPos, this.yPos, focus.xPos, focus.yPos);}
		float distanceTiles(COMMON::IFocalPoint@ focus)  {return distancePixels(focus)/32;}	
	
		bool opEquals(jjOBJ@ jjObject) {return this.cid == jjObject.age;}
		bool opEquals(CUSTOM::Object object) {return object.objectID == this.objectID && object.cid == this.cid;}
		
		void updateDefaultBehavior() {
			if(this.jjObject.behavior != CustomObjectBehavior) {
				if(!isNil && this.jjObject.behavior != BEHAVIOR::DEFAULT) {
					this.defaultBehavior = this.jjObject.behavior;
				} else {
					this.defaultBehavior = DO_NOTHING;
				}
			}
		}
		
		//jjOBJ accessors 
		int get_animSpeed() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_animSpeed()");}}				return this.jjObject.animSpeed;}
		HANDLING::Bullet get_bulletHandling() const {if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_bulletHandling()");}}			return this.jjObject.bulletHandling;}
		bool get_causesRicochet() const 			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_causesRicochet()");}}			return this.jjObject.causesRicochet;}
		int get_counter() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_counter()");}}				return this.jjObject.counter;}
		uint8 get_counterEnd() const 				{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_counterEnd()");}}				return this.jjObject.counterEnd;}
		int get_creator() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_creator()");}}				return this.jjObject.creator;}
		int get_creatorID() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_creatorID()");}}				return this.jjObject.creatorID;}
		CREATOR::Type get_creatorType() const		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_creatorType()");}}			return this.jjObject.creatorType;}
		int16 get_curAnim() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_curAnim()");}}				return this.jjObject.curAnim;}
		uint get_curFrame() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_curFrame()");}}				return this.jjObject.curFrame;}
		bool get_deactivates() const 				{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_deactivates()");}}			return this.jjObject.deactivates;}
		int8 get_direction() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_direction()");}}				return this.jjObject.direction;}
		uint8 get_doesHurt() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_doesHurt()");}}				return this.jjObject.doesHurt;}
		int get_energy() const 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_energy()");}}					return this.jjObject.energy;}
		uint8 get_eventID() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_eventID()");}}				return this.jjObject.eventID;}
		int8 get_frameID() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_frameID()");}}				return this.jjObject.frameID;}
		uint8 get_freeze() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_freeze()");}}					return this.jjObject.freeze;}
		bool get_isBlastable() const 				{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_isBlastable()");}}			return this.jjObject.isBlastable;}
		bool get_isFreezable() const 				{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_isFreezable()");}}			return this.jjObject.isFreezable;}
		bool get_isTarget() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_isTarget()");}}				return this.jjObject.isTarget;}
		uint8 get_justHit() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_justHit()");}}				return this.jjObject.justHit;}
		int16 get_killAnim() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_killAnim()");}}				return this.jjObject.killAnim;}
		int8 get_light() const 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_light()");}}					return this.jjObject.light;}
		uint8 get_lightType() const 				{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_lightType()");}}				return this.jjObject.lightType;}
		int get_oldState() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_oldState()");}}				return this.jjObject.oldState;}
		HANDLING::Player get_playerHandling() const {if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_playerHandling()");}}			return this.jjObject.playerHandling;}
		uint16 get_points() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_points()");}}					return this.jjObject.points;}
		int8 get_noHit() const 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_noHit()");}}					return this.jjObject.noHit;}
		uint16 get_objectID() const 				{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_objectID()");}}				return this.jjObject.objectID;}
		uint8 get_objType() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_objType()");}}				return this.jjObject.objType;}
		bool get_scriptedCollisions() const 		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_scriptedCollisions()");}}		return this.jjObject.scriptedCollisions;}
		int get_special() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_special()");}}				return this.jjObject.special;}
		int get_state() const 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_state()");}}					return this.jjObject.state;}
		bool get_triggersTNT() const 				{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_triggersTNT()");}}			return this.jjObject.triggersTNT;}
		int get_var(int index) const 				{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_var()");}}					return this.jjObject.var[index];}
		float get_xAcc() const 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_xAcc()");}}					return this.jjObject.xAcc;}
		float get_xOrg() const 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_xOrg()");}}					return this.jjObject.xOrg;}
		float get_xPos() const 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_xPos()");}}					return this.jjObject.xPos;}
		float get_xSpeed() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_xSpeed()");}}					return this.jjObject.xSpeed;}
		float get_yAcc() const 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_yAcc()");}}					return this.jjObject.yAcc;}
		float get_yPos() const 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_yPos()");}}					return this.jjObject.yPos;}
		float get_yOrg() const 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_yOrg()");}}					return this.jjObject.yOrg;}
		float get_ySpeed() const 					{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via get_ySpeed()");}}					return this.jjObject.ySpeed;}
		
		//jjOBJ mutators
		void set_animSpeed(int animSpeed) 						 	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_animSpeed()");}return;}			this.jjObject.animSpeed=animSpeed;}
		void set_bulletHandling(HANDLING::Bullet bulletHandling) 	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_bulletHandling()");}return;}		this.jjObject.bulletHandling=bulletHandling;}
		void set_causesRicochet(bool causesRicochet) 			 	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_causesRicochet()");}return;}		this.jjObject.causesRicochet=causesRicochet;}
		void set_counter(int counter) 							 	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_counter()");}return;}				this.jjObject.counter=counter;}
		void set_counterEnd(uint8 counterEnd) 					 	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_counterEnd()");}return;}			this.jjObject.counterEnd=counterEnd;}
		void set_creator(int creator) 						     	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_creator()");}return;}				this.jjObject.creator=creator;}
		void set_creatorID(int creatorID) 							{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_creatorID()");}return;}			this.jjObject.creatorID=creatorID;}
		void set_creatorType(CREATOR::Type creatorType) 			{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_creatorType()");}return;}			this.jjObject.creatorType=creatorType;}
		void set_curAnim(int16 curAnim) 							{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_curAnim()");}return;}				this.jjObject.curAnim=curAnim;}
		void set_curFrame(uint curFrame) 							{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_curFrame()");}return;}			this.jjObject.curFrame=curFrame;}
		void set_deactivates(bool deactivates) 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_deactivates()");}return;}			this.jjObject.deactivates=deactivates;}
		void set_direction(int8 direction) 							{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_direction()");}return;}			this.jjObject.direction=direction;}
		void set_doesHurt(uint8 doesHurt) 							{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_doesHurt()");}return;}			this.jjObject.doesHurt=doesHurt;}
		void set_energy(int energy) 								{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_energy()");}return;}				this.jjObject.energy=energy;}
		void set_eventID(uint8 eventID) 							{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_eventID()");}return;}				this.jjObject.eventID=eventID;}
		void set_frameID(int8 frameID) 								{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_frameID()");}return;}				this.jjObject.frameID=frameID;}
		void set_freeze(uint8 freeze) 								{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_freeze()");}return;}				this.jjObject.freeze=freeze;}
		void set_isBlastable(bool isBlastable) 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_isBlastable()");}return;}			this.jjObject.isBlastable=isBlastable;}
		void set_isFreezable(bool isFreezable) 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_isFreezable()");}return;}			this.jjObject.isFreezable=isFreezable;}
		void set_isTarget(bool isTarget) 							{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_isTarget()");}return;}			this.jjObject.isTarget=isTarget;}
		void set_justHit(uint8 justHit) 							{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_justHit()");}return;}				this.jjObject.justHit=justHit;}
		void set_killAnim(int16 killAnim) 							{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_killAnim()");}return;}			this.jjObject.killAnim=killAnim;}
		void set_light(int8 light) 									{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_light()");}return;}				this.jjObject.light=light;}
		void set_lightType(uint8 lightType) 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_lightType()");}return;}			this.jjObject.lightType=lightType;}
		void set_oldState(STATE::State state) 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_oldState()");}return;}			this.jjObject.oldState = state;}
		void set_playerHandling(HANDLING::Player playerHandling) 	{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_playerHandling()");}return;}		this.jjObject.playerHandling=playerHandling;}
		void set_points(uint16 points) 								{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_points()");}return;}				this.jjObject.points=points;}
		void set_noHit(int8 noHit) 									{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_noHit()");}return;}				this.jjObject.noHit=noHit;}
		void set_objType(uint8 objType) 							{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_objType()");}return;}				this.jjObject.objType=objType;}
		void set_scriptedCollisions(bool scriptedCollisions) 		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_scriptedCollisions()");}return;}	this.jjObject.scriptedCollisions=scriptedCollisions;}
		void set_special(int special) 								{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_special()");}return;}				this.jjObject.special=special;}
		void set_state(int state) 									{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_state()");}return;}				this.jjObject.state = state;}
		void set_triggersTNT(bool triggersTNT) 						{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_triggersTNT()");}return;}			this.jjObject.triggersTNT=triggersTNT;}
		void set_var(int index, int value) 							{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_var()");}return;}					this.jjObject.var[index]=value;}
		void set_xAcc(float xAcc) 									{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_xAcc()");}return;}				this.jjObject.xAcc=xAcc;}
		void set_xOrg(float xOrg) 									{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_xOrg()");}return;}				this.jjObject.xOrg=xOrg;}
		void set_xPos(float xPos) 									{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_xPos()");}return;}				this.jjObject.xPos=xPos;}
		void set_xSpeed(float xSpeed) 								{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_xSpeed()");}return;}				this.jjObject.xSpeed=xSpeed;}
		void set_ySpeed(float ySpeed) 								{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_ySpeed()");}return;}				this.jjObject.ySpeed=ySpeed;}
		void set_yAcc(float yAcc) 									{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_yAcc()");}return;}				this.jjObject.yAcc=yAcc;}
		void set_yPos(float yPos) 									{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_yPos()");}return;}				this.jjObject.yPos=yPos;}
		void set_yOrg(float yOrg) 									{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via set_yOrg()");}return;}				this.jjObject.yOrg=yOrg;}
		
		//jjOBJ methods
		void clearPlatform() const									{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via clearPlatform()");}return;}			this.jjObject.clearPlatform();}
		void grantPickup(jjPLAYER@ player, int frequency) const		{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via grantPickup()");}return;}				this.jjObject.grantPickup(player, frequency);}
		void particlePixelExplosion(int style) const				{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via particlePixelExplosion()");}return;}	this.jjObject.particlePixelExplosion(style);}
		void pathMovement() const									{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via pathMovement()");}return;}			this.jjObject.pathMovement();}
		void putOnGround(bool precise = false) const				{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via putOnGround()");}return;}				this.jjObject.putOnGround(precise);}
		void delete() 												{if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via delete()");}return;}					this.jjObject.delete();}
		
		void deleteNoWarn() {if(!objectChanged(this.jjObject)) this.jjObject.delete();}
		
		int 	beSolid() const																	 {if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via beSolid()");}return  0;}			return this.jjObject.beSolid();}
		int16 	determineCurAnim(uint8 setID, uint8 animation, bool change = true) const		 {if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via determineCurAnim()");}return -1;}	return this.jjObject.determineCurAnim(setID,animation,change);}
		int16 	determineCurAnim(ANIM::Set setID, uint8 animation, bool change = true) const	 {if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via determineCurAnim()");}return -1;}	return this.jjObject.determineCurAnim(setID,animation,change);}
		uint 	determineCurFrame(bool change = true) const									 	 {if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via determineCurFrame()");}return 0;}	return this.jjObject.determineCurFrame(change);}
		int 	findNearestPlayer(int maxDistance) const										 {if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via findNearestPlayer()");}return -1;}	return this.jjObject.findNearestPlayer(maxDistance);}
		int 	findNearestPlayer(int maxDistance, int &out foundDistance) const				 {if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via findNearestPlayer()");}return -1;}	return this.jjObject.findNearestPlayer(maxDistance, foundDistance);}
		int 	fireBullet(OBJECT::Object eventID) const										 {if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via fireBullet()");}return -1;}			return this.jjObject.fireBullet(eventID);}
		int 	unfreeze(int style) const														 {if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via unfreeze()");}return -1;}			return this.jjObject.unfreeze(style);}
		bool 	ricochet() const																 {if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via ricochet()");}return false;}		return this.jjObject.ricochet();}
	
		//Reimplemented deactivate to explicitly call delete, so if delete is overridden it will still work.
		//Also prevents having to override both deactivate and delete (just delete is enough)
		void deactivate() {
			if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via deactivate()");}return;}
			this.jjObject.delete();
			if(this.jjObject.creatorType == CREATOR::LEVEL) {
			  jjEventSet(int(this.jjObject.xOrg/32), int(this.jjObject.yOrg/32), this.jjObject.eventID);
			  jjParameterSet(int(this.jjObject.xOrg/32), int(this.jjObject.yOrg/32), -1, 1, 0);
			}
		}
		
		//Reimplemented behave to explicitly call draw()
		void behave(BEHAVIOR::Behavior behavior, bool draw = true, bool useCustomSpriteProperties = true) const {
			if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via behave()");}return;}
			if(!isWarping()) this.jjObject.behave(behavior, false);
			if(draw && isActive) this.draw(useCustomSpriteProperties);
		}
		
		void behave(jjVOIDFUNCOBJ@ behavior, bool draw = true, bool useCustomSpriteProperties = true) const {
			if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via behave()");}return;}
			if(!isWarping()) this.jjObject.behave(behavior, false);
			if(draw && isActive) this.draw(useCustomSpriteProperties);
		}
		
		void behave(jjBEHAVIOR behavior, bool draw = true, bool useCustomSpriteProperties = true) const {
			if(objectChanged(this.jjObject)){if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via behave()");}return;}
			if(!isWarping()) this.jjObject.behave(behavior, false);
			if(draw && isActive) this.draw(useCustomSpriteProperties);
		}

		//Reimplemented draw to use custom sprite properties
		int draw(bool useCustomSpriteProperties = true) const {
			if(objectChanged(this.jjObject)) {
				if(CUSTOM::SHOW_WARNINGS){jjDebug("SaLLiB WARNING: Tried to access a deleted object via draw()");}
				return -1;
			}
			if(invisible) return -1;
			if(isPlayingAnimation() || useCustomSpriteProperties) {
				float offset = bob ? this.bobInterpolator.value(bobTimer.elapsedTime()) : 0;
				SPRITE::Mode mode = justHit != 0 ? SPRITE::SINGLECOLOR : this.mode;
				int param = justHit != 0 ? 64 : this.param;
				int angleDirection = direction > 0 ? angle : -angle;
				float xScaleDirection = direction > 0 ? scaleX : -1*scaleX;
				if(angle != 0) {
					if(isPlayingAnimation())
						jjDrawRotatedSprite(xPos,yPos+offset,st,anim,frame,angleDirection,xScaleDirection,scaleY,mode,param,layerZ,layerXY,playerID); 
					else
						jjDrawRotatedSpriteFromCurFrame(xPos,yPos+offset,curFrame,angleDirection,xScaleDirection,scaleY,mode,param,layerZ,layerXY,playerID);
					return -1;
				} else if(scaleX != 1 || scaleY != 1) {
					if(isPlayingAnimation())
						jjDrawResizedSprite(xPos,yPos+offset,st,anim,frame,xScaleDirection,scaleY,mode,param,layerZ,layerXY,playerID);
					else
						jjDrawResizedSpriteFromCurFrame(xPos,yPos+offset,curFrame,xScaleDirection,scaleY,mode,param,layerZ,layerXY,playerID);
					return -1;
				} else if(isPlayingAnimation() || bob != false || mode != SPRITE::NORMAL || layerZ != 4 || layerXY != 4 || playerID != -1) {
					if(isPlayingAnimation())
						jjDrawSprite(xPos,yPos+offset,st,anim,frame,direction,mode,param,layerZ,layerXY,playerID);
					else
						jjDrawSpriteFromCurFrame(xPos,yPos+offset,curFrame,direction,mode,param,layerZ,layerXY,playerID);
					return -1;
				} else { return this.jjObject.draw(); }
			} else { return this.jjObject.draw(); }
		}
		
		//Debug helpers
		void printPos() {
			jjDebug((xPos/32) + "," + (yPos/32));
		}
	}
	
	class Boss : CUSTOM::Object {
		int oldPresetEnergy;
		jjPLAYER@ player;
		bool activated = false;
		GAME::IBossMeter@ meter = null;
		
		Boss(OBJECT::Object objectTemplate, float xPixel, float yPixel) {
			super(objectTemplate, xPixel, yPixel);
			jjObjectPresets[255].determineCurAnim(ANIM::BOSS, 0);
			this.playerHandling = HANDLING::SPECIAL;
			this.bulletHandling = HANDLING::DETECTBULLET;
			this.scriptedCollisions = true;
		}
		
		int get_energy() {
			if(meter is null) return Object::get_energy();
			return meter.energy;
		}
		
		void set_energy(int energy) {
			if(meter is null) Object::set_energy(energy);
			else meter.energy = energy;
		}
		
		void activateBoss(int maxEnergy) {
			activateBossWithUIMeter(maxEnergy,10*32,0);
		}
		
		void activateBossWithUIMeter(int maxEnergy, float xPos=10*32, float yPos=0) {
			if(!activated) {
				GAME::BossMeterUI@ meterUI = @GAME::BossMeterUI(xPos,yPos);
				@meter = @meterUI;
				CUSTOM::UI.addElement(meterUI);
				meter.maxEnergy = maxEnergy;
				meter.energy = maxEnergy;
				activated = true;
			}
		}
		
		void activateBossWithFloatingMeter(int maxEnergy, int yOffset = 0) {
			if(!activated) {
				@meter = @GAME::BossMeterFloating(this,yOffset);
				meter.maxEnergy = maxEnergy;
				meter.energy = maxEnergy;
				activated = true;
			}
		}
		
		void deactivateBoss() {
			if(activated) {
				GAME::BossMeterUI@ casted = cast<GAME::BossMeterUI@>(meter);
				if(casted !is null) {
					CUSTOM::UI.removeElement(casted);
				}
				meter.delete();
				@meter = null;
				activated = false;
			}
		}
		
		GAME::IBossMeter@ bossMeter() {
			return meter;
		}
		
		void delete() {
			deactivateBoss();
			CUSTOM::Object::delete();
		}
		
		void onObjectHit(jjOBJ@ bullet, jjPLAYER@ player, int force) {
			if(bullet !is null && player !is null) {
				if(meter !is null) {
					meter.energy = meter.energy - force;
					justHit = 5;
					bullet.state = STATE::EXPLODE;
					if(meter.energy <= 0) {
						particlePixelExplosion(0);
						delete();
					}
				} else {
					bullet.state = STATE::EXPLODE;
				}
			} else if (player !is null) {
				player.hurt();
			}
		}
	}
	
	funcdef se::colorEffect@ COLOR_EFFECT(jjPALCOLOR,float);

	se::colorEffect@ effectBrighten(jjPALCOLOR color, float value) {return se::colorBrighten(value);}
	se::colorEffect@ effectContrast(jjPALCOLOR color, float value) {return se::colorContrast(value);}
	se::colorEffect@ effectFade(jjPALCOLOR color, float value) 	   {return se::colorFade(color,value);}
	se::colorEffect@ effectGamma(jjPALCOLOR color,float value)     {return se::colorGamma(value);}
	se::colorEffect@ effectShiftHue(jjPALCOLOR color,float value)  {return se::colorShiftHue(int(value));}
	se::colorEffect@ effectSketch(jjPALCOLOR color,float value)    {return se::colorSketch(int(value));}
	
	class Palette : CUSTOM::Object {
		INTERPOLATION::Interpolator@ interpolatorEffect;
		COMMON::Timer effectTimer();
		bool effectCycle;
		jjPAL palette;
		jjPALCOLOR palcolor;
		COLOR_EFFECT@ effectFunction;
		int minColor, maxColor;
		int updateDelay = 3;

		Palette() {
			super(NIL,-1,-1);
		}
		
		void set_delay(int delay) { this.updateDelay = delay; }
		int  get_delay()		  { return this.updateDelay;  }
		
		void colorGrayscale(int minColor = 0, int maxColor = 255) 		  				{applyColorEffect(se::colorGrayscale(), minColor, maxColor);}
		void colorSolarize(int minColor = 0, int maxColor = 255)  		  				{applyColorEffect(se::colorSolarize(), minColor, maxColor);}
		void colorSepia(int minColor = 0, int maxColor = 255)  	  		  				{applyColorEffect(se::colorSepia(), minColor, maxColor);}
		void colorNegative(int minColor = 0, int maxColor = 255)  		  				{applyColorEffect(se::colorNegative(), minColor, maxColor);}
		void colorDecompositionMin(int minColor = 0, int maxColor = 255)  				{applyColorEffect(se::colorDecompositionMin(), minColor, maxColor);}
		void colorDecompositionMax(int minColor = 0, int maxColor = 255)  				{applyColorEffect(se::colorDecompositionMax(), minColor, maxColor);}
		void colorGlow(jjPALCOLOR color, int minColor = 0, int maxColor = 255)			{applyColorEffect(se::colorGlow(color), minColor, maxColor);}
		void colorReduction(int r, int g, int b,int minColor = 0, int maxColor = 255)  	{applyColorEffect(se::colorReduction(r,g,b), minColor, maxColor);}
		void colorReplace(jjPALCOLOR color, int minColor = 0, int maxColor = 255)		{applyColorEffect(se::colorReplace(color), minColor, maxColor);}
		
		void restore() {
			jjBackupPalette.apply();
		}
		
		void applyColorEffect(se::colorEffect@ effect, uint minColor = 1, uint maxColor = 256) {
			jjPAL newPalette = jjBackupPalette;
			for(uint i = minColor; i < maxColor; i++) {
				newPalette.color[i] = effect.getResult(jjBackupPalette.color[i]);
			}
			newPalette.apply();
		}
		
		void brightenLinear(jjPAL palette, float min, float max, int duration, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			effectInterpolator(effectBrighten,
							   palette, 
							   jjPALCOLOR(),
							   INTERPOLATION::Linear(min,max,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void brightenOscillate(jjPAL palette, jjPALCOLOR color, float min, float max, int duration, float frequency, int offset, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			float dist = (max-min)/2;
			float mid  = min + dist;
			effectInterpolator(effectBrighten, 
							   palette,
							   color,
							   INTERPOLATION::PointPlusSine(COMMON::Position(mid,mid),INTERPOLATION::getX,frequency,dist,offset,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void contrastLinear(jjPAL palette, float min, float max, int duration, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			effectInterpolator(effectContrast,
							   palette, 
							   jjPALCOLOR(),
							   INTERPOLATION::Linear(min,max,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void contrastOscillate(jjPAL palette, jjPALCOLOR color, float min, float max, int duration, float frequency, int offset, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			float dist = (max-min)/2;
			float mid  = min + dist;
			effectInterpolator(effectContrast, 
							   palette,
							   color,
							   INTERPOLATION::PointPlusSine(COMMON::Position(mid,mid),INTERPOLATION::getX,frequency,dist,offset,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void fadeLinear(jjPAL palette, jjPALCOLOR color, float min, float max, int duration, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			effectInterpolator(effectFade, 
							   palette,
							   color,
							   INTERPOLATION::Linear(min,max,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void fadeOscillate(jjPAL palette, jjPALCOLOR color, float min, float max, int duration, float frequency, int offset, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			float dist = (max-min)/2;
			float mid  = min + dist;
			effectInterpolator(effectFade, 
							   palette,
							   color,
							   INTERPOLATION::PointPlusSine(COMMON::Position(mid,mid),INTERPOLATION::getX,frequency,dist,offset,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void gammaLinear(jjPAL palette, float min, float max, int duration, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			effectInterpolator(effectGamma,
							   palette, 
							   jjPALCOLOR(),
							   INTERPOLATION::Linear(min,max,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void gammaOscillate(jjPAL palette, float min, float max, int duration, float frequency, int offset, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			float dist = (max-min)/2;
			float mid  = min + dist;
			effectInterpolator(effectGamma, 
							   palette,
							   jjPALCOLOR(),
							   INTERPOLATION::PointPlusSine(COMMON::Position(mid,mid),INTERPOLATION::getX,frequency,dist,offset,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void shiftHueLinear(jjPAL palette, float min, float max, int duration, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			effectInterpolator(effectShiftHue,
							   palette, 
							   jjPALCOLOR(),
							   INTERPOLATION::Linear(min,max,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void shiftHueOscillate(jjPAL palette, float min, float max, int duration, float frequency, int offset, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			float dist = (max-min)/2;
			float mid  = min + dist;
			effectInterpolator(effectShiftHue, 
							   palette,
							   jjPALCOLOR(),
							   INTERPOLATION::PointPlusSine(COMMON::Position(mid,mid),INTERPOLATION::getX,frequency,dist,offset,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void sketchLinear(jjPAL palette, float min, float max, int duration, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			effectInterpolator(effectSketch,
							   palette, 
							   jjPALCOLOR(),
							   INTERPOLATION::Linear(min,max,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void sketchOscillate(jjPAL palette, float min, float max, int duration, float frequency, int offset, uint minColor = 1, uint maxColor = 256, bool cycle = false) {
			float dist = (max-min)/2;
			float mid  = min + dist;
			effectInterpolator(effectSketch, 
							   palette,
							   jjPALCOLOR(),
							   INTERPOLATION::PointPlusSine(COMMON::Position(mid,mid),INTERPOLATION::getX,frequency,dist,offset,duration),
							   duration,minColor,maxColor,cycle);
		}
		
		void effectInterpolator(COLOR_EFFECT@ colorEffect, jjPAL palette, jjPALCOLOR color, INTERPOLATION::Interpolator@ interpolator, int duration, int minColor = 0, int maxColor = 255, bool cycle = false) {
			@effectFunction = @colorEffect;
			this.minColor = minColor;
			this.maxColor = maxColor;
			this.palcolor = color;
			this.palette = palette;
			effectTimer.start(duration);
			@this.interpolatorEffect = interpolator;
			effectCycle = cycle;
		}
			
		bool effect() {
			if(effectTimer.isStarted()) {
				if(effectTimer.elapsedTime() == 0 || effectTimer.elapsedTime() == effectTimer.interval() || effectTimer.elapsedTime() % updateDelay == 0) {
					if(effectTimer.elapsedTime() <= effectTimer.interval() || effectTimer.interval() == -1) {
						se::colorEffect@ ceffect = effectFunction(this.palcolor,interpolatorEffect.value(effectTimer.elapsedTime()));
						applyColorEffect(ceffect,minColor,maxColor);
						return true;
					} else if(effectCycle && effectTimer.elapsedTime() > effectTimer.interval()) {
						effectTimer.reset();
						se::colorEffect@ ceffect = effectFunction(this.palcolor,interpolatorEffect.value(effectTimer.elapsedTime()));
						applyColorEffect(ceffect,minColor,maxColor);
						return true;
					}
				}
			}
			return false;
		}
		
		bool isPerformingColorEffect() {
			return effectTimer.isStarted() && !effectTimer.isFinished();
		}
		
		void stopEffect() {
			effectTimer.stop();
		}
		
		void behavior() {
			effect();
		}
	}

	class Player : COMMON::IFocalPoint {
		jjPLAYER@ jjPlayer;
		
		Player(jjPLAYER@ player)  {@this.jjPlayer = @player;}
		
		void set_xPos(float xPos) {this.jjPlayer.xPos=xPos;}
		void set_yPos(float yPos) {this.jjPlayer.yPos=yPos;}
		
		float get_xPos() const    {return this.jjPlayer.xPos;}
		float get_yPos() const    {return this.jjPlayer.yPos;}
		
		bool get_isActive() const {return this.jjPlayer.isActive;}
	}
	
	funcdef CUSTOM::Object@ BULLET(jjOBJ@ bullet);
	
	void CustomBulletBehavior1(jjOBJ@ bullet) { CustomBulletBehavior(bullet, 1); }
	void CustomBulletBehavior2(jjOBJ@ bullet) { CustomBulletBehavior(bullet, 2); }
	void CustomBulletBehavior3(jjOBJ@ bullet) { CustomBulletBehavior(bullet, 3); }
	void CustomBulletBehavior4(jjOBJ@ bullet) { CustomBulletBehavior(bullet, 4); }
	void CustomBulletBehavior5(jjOBJ@ bullet) { CustomBulletBehavior(bullet, 5); }
	void CustomBulletBehavior6(jjOBJ@ bullet) { CustomBulletBehavior(bullet, 6); }
	void CustomBulletBehavior7(jjOBJ@ bullet) { CustomBulletBehavior(bullet, 7); }
	void CustomBulletBehavior8(jjOBJ@ bullet) { CustomBulletBehavior(bullet, 8); }
	void CustomBulletBehavior9(jjOBJ@ bullet) { CustomBulletBehavior(bullet, 9); }
	
	void CustomBulletBehavior(jjOBJ@ bullet, int weaponNumber) {
		CUSTOM::weapons.get(weaponNumber).getBullet()(bullet);
	}
	
	class Weapon {
		int gun;
		BULLET@ makeBullet;
		IInterfaceElement@ element;
		
		Weapon(int gun, BULLET@ bullet, IInterfaceElement@ element = null) {
			this.gun = gun;
			@this.makeBullet = @bullet;
			@this.element = @element;
		}
		
		//Can't use virtual accessor with function handle or it crashes.
		BULLET@		  		getBullet()				{ return makeBullet; }
		IInterfaceElement@	get_interfaceElement()		{ return element; }
		
		bool 		  get_comesFromGunCrates()  { return jjWeapons[gun].comesFromGunCrates; }
		bool 		  get_infinite()		    { return jjWeapons[gun].infinite; }
		int 		  get_maximum()			    { return jjWeapons[gun].maximum; }
		int 		  get_multiplier()		 	{ return jjWeapons[gun].multiplier; }
		bool 		  get_replacedByBubbles()	{ return jjWeapons[gun].replacedByBubbles; }
		bool 		  get_replenishes()			{ return jjWeapons[gun].replenishes; }
		int			  get_style()				{ return jjWeapons[gun].style; }
		
		void 			set_comesFromGunCrates(bool comesFromGunCrates) { jjWeapons[gun].comesFromGunCrates = comesFromGunCrates; }
		void 			set_infinite(bool infinite)						{ jjWeapons[gun].infinite = infinite; }
		void  			set_maximum(int maximum)						{ jjWeapons[gun].maximum = maximum; }
		void  			set_multiplier(int multiplier)					{ jjWeapons[gun].multiplier = multiplier; }
		void 			set_replacedByBubbles(bool replacedByBubbles)	{ jjWeapons[gun].replacedByBubbles = replacedByBubbles; }
		void 			set_replenishes(bool replenishes)				{ jjWeapons[gun].replenishes = replenishes; }
		void		 	set_style(int style)							{ jjWeapons[gun].style = style; }
	}
	
	class Weapons {
		array<CUSTOM::Weapon@> weapons(10);
		
		Weapons() {
			for(uint i = 1; i <= 9; i++)
				@weapons[i] = @Weapon(i, null, null);
		}
		
		void registerWeapon(int gun, BULLET@ bullet, IInterfaceElement@ element = null) {
			if(gun < 1 || gun > 9) 
				if(SHOW_WARNINGS)
					jjDebug("SaLLiB Warning: Button out of range in custom weapon!");
			@weapons[gun] = @CUSTOM::Weapon(gun,bullet,element);
			if(gun == 1) 	  jjObjectPresets[OBJECT::BLASTERBULLET].behavior = CustomBulletBehavior1;
			else if(gun == 2) jjObjectPresets[OBJECT::BOUNCERBULLET].behavior = CustomBulletBehavior2;
			else if(gun == 3) jjObjectPresets[OBJECT::ICEBULLET].behavior = CustomBulletBehavior3;
			else if(gun == 4) jjObjectPresets[OBJECT::SEEKERBULLET].behavior = CustomBulletBehavior4;
			else if(gun == 5) jjObjectPresets[OBJECT::RFBULLET].behavior = CustomBulletBehavior5;
			else if(gun == 6) jjObjectPresets[OBJECT::TOASTERBULLET].behavior = CustomBulletBehavior6;
			else if(gun == 7) jjObjectPresets[OBJECT::TNT].behavior = CustomBulletBehavior7;
			else if(gun == 8) jjObjectPresets[OBJECT::FIREBALLBULLET].behavior = CustomBulletBehavior8;
			else if(gun == 9) jjObjectPresets[OBJECT::ELECTROBULLET].behavior = CustomBulletBehavior9;
		}
		
		CUSTOM::Weapon@ get(int gun) {
			return weapons[gun];
		}
	}
	
	Weapons weapons;
	
	interface IInterfaceElement {
		void draw(jjPLAYER@ player, jjCANVAS@ canvas);
	}
	
	class WeaponDisplayInterfaceElement : IInterfaceElement {
		void draw(jjPLAYER@ player, jjCANVAS@ canvas) {	
			IInterfaceElement@ element = @CUSTOM::weapons.get(player.currWeapon).interfaceElement;
			if(element !is null) {
				CUSTOM::UI.drawAmmo = false;
				element.draw(player,canvas);
			} else {
				CUSTOM::UI.drawAmmo = true;
			}
		}
	}
	
	interface IInterface {
		bool get_drawScore() const;
		bool get_drawHealth() const;
		bool get_drawLives() const;
		bool get_drawAmmo() const;
		bool get_drawPlayerTimer() const;
		
		int get_numElements() const;
		
		void set_drawScore(bool drawScore);
		void set_drawHealth(bool drawHealth);
		void set_drawLives(bool drawLives);
		void set_drawAmmo(bool drawAmmo);
		void set_drawPlayerTimer(bool drawPlayerTimer);
		
		void addElement(IInterfaceElement@ element);
		void removeElement(IInterfaceElement@ element);
		
		void draw(jjPLAYER@ player,jjCANVAS@ canvas);
	}
	
	class Interface : IInterface {
		array<CUSTOM::IInterfaceElement@> ui();
	
		bool drawScore { get const {return drawScore;} set {drawScore = value; }}
		bool drawScore = true;
		
		bool drawHealth { get const {return drawHealth;} set {drawHealth = value; }}
		bool drawHealth = true;
		
		bool drawLives { get const {return drawLives;} set {drawLives = value;}}
		bool drawLives = true;
		
		bool drawAmmo { get const {return drawAmmo;} set {drawAmmo = value;}}
		bool drawAmmo = true;
		
		bool drawPlayerTimer { get const { return drawPlayerTimer;} set {drawPlayerTimer = value;}}
		bool drawPlayerTimer = true;
	
		int numElements { get const {return ui.length;}}
	
		void addElement(IInterfaceElement@ element) {ui.insertLast(element);}
		
		void removeElement(IInterfaceElement@ element) {
			//Workaround bug in angelscript's array.find
			int index = -1;
			for(uint i = 0; i < ui.length; i++)
				if(ui[i] is element) {
					index = i;
					break;
				}
			if(index >= 0)
				ui.removeAt(index);
		}
	
		void draw(jjPLAYER@ player,jjCANVAS@ canvas) {
			for(uint i = 0; i < ui.length; i++) {
				ui[i].draw(player,canvas);
			}
		}
	}
	
	class EnhancedUI : Interface {
		EnhancedUI() {
			this.addElement(WeaponDisplayInterfaceElement());
		}
	}
	
	IInterface@ UI = @Interface();
}

bool onDrawAmmo(jjPLAYER@ player, jjCANVAS@ canvas) {
	CUSTOM::UI.draw(player,canvas);
	if(onDrawAmmoHook !is null) onDrawAmmoHook(player,canvas);
	return !CUSTOM::UI.drawAmmo;
}

bool onDrawHealth(jjPLAYER@ player, jjCANVAS@ canvas) {
	if(onDrawHealthHook !is null) onDrawHealthHook(player,canvas);
	return !CUSTOM::UI.drawHealth;
}

bool onDrawLives(jjPLAYER@ player, jjCANVAS@ canvas) {
	if(onDrawLivesHook !is null) onDrawLivesHook(player,canvas);
	return !CUSTOM::UI.drawLives;
}

bool onDrawPlayerTimer(jjPLAYER@ player, jjCANVAS@ canvas) {
	if(onDrawPlayerTimerHook !is null) onDrawPlayerTimerHook(player,canvas);
	return !CUSTOM::UI.drawPlayerTimer;
}

bool onDrawScore(jjPLAYER@ player, jjCANVAS@ canvas) {
	if(onDrawScoreHook !is null) onDrawScoreHook(player,canvas);
	return !CUSTOM::UI.drawScore;
}

void onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
	CUSTOM::objects[obj.objectID].onObjectHit(bullet,player,force);
	if(onObjectHitHook !is null) onObjectHitHook(obj,bullet,player,force);
}

namespace INTERPOLATION {
	interface Interpolator {
		float value(int time);
	}
	
	interface PathInterpolator : Interpolator {
		void  reset(float start, float end, int duration);
	}

	class Linear : PathInterpolator {
		float start, end;
		int duration;
		
		Linear(float start, float end, int duration) {
			reset(start,end,duration);
		}
		
		void reset(float start, float end, int duration) {
			this.start = start;
			this.end = end;
			this.duration = duration;
		}
		
		float value(int time) {
			return ((end-start)/duration)*time + start;
		}
	}
	
	class Exponential : PathInterpolator {
		float start, end, base;
		int duration;
		
		Exponential(float start, float end, float base, int duration) {
			this.base = base;
			reset(start,end,duration);
		}
		
		void reset(float start, float end, int duration) {
			this.start = start;
			this.end = end;
			this.duration = duration;
		}
		
		float logb(float x,float b){
		  return log(x)/log(b);
		}
		
		float value(int time) {
			//Same as this equation, but uses logs to avoid underflow when dividing by pow
			//return ((end-start)/pow(base,duration))*pow(base,time) + start;
			return pow(base,logb(end-start,base)-duration+time)+start;
		}
	}
	
	class Logarithmic : PathInterpolator {
		float start, end, base;
		int duration;
		
		Logarithmic(float start, float end, float base, int duration) {
			this.base = base;
			reset(start,end,duration);
		}
		
		void reset(float start, float end, int duration) {
			this.start = start;
			this.end = end;
			this.duration = duration;
		}
		
		float logb(float x,float b){
		  return log(x)/log(b);
		}
		
		float value(int time) {
			return ((end-start)/logb(duration,base))*logb(time,base) + start;
		}
	}
	
	class Sinusoidal : Interpolator {
		float frequency, amplitude, offset;
		int duration;
		
		Sinusoidal(float frequency, float amplitude, float offset, int duration) {
			reset(frequency,amplitude,offset,duration);
		}
		
		void reset(float frequency, float amplitude, float offset, int duration) {
			this.frequency = frequency;
			this.amplitude = amplitude;
			this.offset = offset;
			this.duration = duration;
		}
		
		float value(int time) {
			return amplitude*sin(time*frequency*PI/duration + offset);
		}
	}
	
	class LinePlusSine : PathInterpolator {
		Linear@ line;
		Sinusoidal@ sine;
		
		LinePlusSine(float start, float end, int duration, float frequency, float amplitude, float offset) {
			@line = @Linear(start, end, duration);
			@sine = @Sinusoidal(frequency,amplitude,offset,duration);
		}
		
		void reset(float start,float end,int duration) {
			line.reset(start, end, duration);
			sine.duration = duration;
		}
		
		float value(int time) {
			return line.value(time) + sine.value(time);
		}
	}
	
	funcdef float SELECTOR(COMMON::IFocalPoint@);
	
	float getX(COMMON::IFocalPoint@ focus) {return focus.xPos;}
	float getY(COMMON::IFocalPoint@ focus) {return focus.yPos;}
	
	class Point : Interpolator {
		COMMON::IFocalPoint@ focus;
		SELECTOR@ selector;
		
		Point(COMMON::IFocalPoint@ focus, SELECTOR@ selector) {
			@this.focus = @focus;
			@this.selector = @selector;
		}
		
		float value(int time) {
			return selector(focus);
		}
	}
	
	class PointPlusSine : Interpolator {
		COMMON::IFocalPoint@ focus;
		SELECTOR@ selector;
		Sinusoidal@ sine;
		
		PointPlusSine(COMMON::IFocalPoint@ focus, SELECTOR@ selector, float frequency, float amplitude, float offset, int duration) {
			@sine = @Sinusoidal(frequency,amplitude,offset,duration);
			@this.selector = @selector;
			@this.focus = @focus;
		}
		
		void reset(float frequency, float amplitude, float offset, int duration) {
			sine.reset(frequency,amplitude,offset,duration);
		}
		
		float value(int time) {
			return selector(focus) + sine.value(time);
		}
	}
}

namespace MIXIN {
	interface IMovable {
		void moveLinear(float xStart, float yStart, float xEnd, float yEnd, int duration, bool cycle = false);
		void moveLinear(float xEnd, float yEnd, int duration, bool cycle = false);
		void moveAccelerate(float xStart, float yStart, float xEnd, float yEnd, int duration, float acceleration, bool cycle = false);
		void moveAccelerate(float xEnd, float yEnd, int duration, float acceleration, bool cycle = false);
		void moveDeccelerate(float xStart, float yStart, float xEnd, float yEnd, int duration, float decceleration, bool cycle = false);
		void moveDeccelerate(float xEnd, float yEnd, int duration, float decceleration, bool cycle = false);
		void moveSinusoidal(float xStart, float yStart, float xEnd, float yEnd, int duration, float frequency, float amplitude, float offset, bool cycle = false);
		void moveSinusoisal(float xEnd, float yEnd, int duration, float frequency, float amplitude, float offset, bool cycle = false);
		void moveSpiral(float xStart, float yStart, float xEnd, float yEnd, int duration, float frequency, float amplitude, float offset, bool cycle = false);
		void moveSpiral(float xEnd, float yEnd, int duration, float frequency, float amplitude, float offset, bool cycle = false);
		void moveRevolve(COMMON::IFocalPoint@ focus, int duration, float radius, float offset, bool clockwise, bool cycle = false);
		void moveOscillate(COMMON::IFocalPoint@ focus, int duration, float xDist, float yDist, float frequency, float offset, bool cycle = false);
		void moveFollow(COMMON::IFocalPoint@ focus);
		void moveInterpolator(INTERPOLATION::Interpolator@ xInterpolator, INTERPOLATION::Interpolator@ yInterpolator, int duration, bool cycle = false);
		bool move();
		bool isMoving();
		void stopMove();
	}
	
	mixin class Movable : IMovable {
		INTERPOLATION::Interpolator@ xInterpolatorMove;
		INTERPOLATION::Interpolator@ yInterpolatorMove;
		COMMON::Timer moveTimer();
		bool moveCycle;
	
		void moveLinear(float xStart, float yStart, float xEnd, float yEnd, int duration, bool cycle = false) {
			moveInterpolator(INTERPOLATION::Linear(xStart,xEnd,duration),
							 INTERPOLATION::Linear(yStart,yEnd,duration),
							 duration,
							 cycle);
			
		}
		
		void moveLinear(float xEnd, float yEnd, int duration, bool cycle = false) {
			moveLinear(xPos,yPos,xEnd,yEnd,duration,cycle);
		}
		
		void moveAccelerate(float xStart, float yStart, float xEnd, float yEnd, int duration, float acceleration, bool cycle = false) {
			moveInterpolator(INTERPOLATION::Exponential(xStart,xEnd,acceleration,duration),
							 INTERPOLATION::Exponential(yStart,yEnd,acceleration,duration),
							 duration,
							 cycle);
		}
		
		void moveAccelerate(float xEnd, float yEnd, int duration, float acceleration, bool cycle = false) {
			moveAccelerate(xPos, yPos, xEnd, yEnd, duration, acceleration, cycle);
		}
		
		void moveDeccelerate(float xStart, float yStart, float xEnd, float yEnd, int duration, float decceleration, bool cycle = false) {
			moveInterpolator(INTERPOLATION::Logarithmic(xStart,xEnd,decceleration,duration),
							 INTERPOLATION::Logarithmic(yStart,yEnd,decceleration,duration),
							 duration,
							 cycle);
		}
		
		void moveDeccelerate(float xEnd, float yEnd, int duration, float decceleration, bool cycle = false) {
			moveDeccelerate(xPos, yPos, xEnd, yEnd, duration, decceleration, cycle);
		}
		
		void moveSinusoidal(float xStart, float yStart, float xEnd, float yEnd, int duration, float frequency, float amplitude, float offset, bool cycle = false) {
			moveInterpolator(INTERPOLATION::LinePlusSine(xStart,xEnd,duration,frequency,amplitude,offset),
							 INTERPOLATION::LinePlusSine(yStart,yEnd,duration,frequency,amplitude,offset),
							 duration,
							 cycle);
		}
		
		void moveSinusoisal(float xEnd, float yEnd, int duration, float frequency, float amplitude, float offset, bool cycle = false) {
			moveSinusoidal(xPos,yPos,xEnd,yEnd,duration,frequency,amplitude,offset,cycle);
		}
		
		void moveSpiral(float xStart, float yStart, float xEnd, float yEnd, int duration, float frequency, float amplitude, float offset, bool cycle = false) {
			moveInterpolator(INTERPOLATION::LinePlusSine(xStart,xEnd,duration,frequency,amplitude,offset),    //sine
							 INTERPOLATION::LinePlusSine(yStart,yEnd,duration,frequency,amplitude,offset+PI), //cosine
							 duration,
							 cycle);
		}
		
		void moveSpiral(float xEnd, float yEnd, int duration, float frequency, float amplitude, float offset, bool cycle = false) {
			moveSpiral(xPos,yPos,xEnd,yEnd,duration,frequency,amplitude,offset,cycle);
		}
		
		void moveRevolve(COMMON::IFocalPoint@ focus, int duration, float radius, float offset, bool clockwise, bool cycle = false) {
			int frequency = 2;
			if(clockwise) frequency *= -1;
			moveInterpolator(INTERPOLATION::PointPlusSine(focus,INTERPOLATION::getX,frequency,radius,offset,duration),
							 INTERPOLATION::PointPlusSine(focus,INTERPOLATION::getY,frequency,radius,offset+HALF_PI,duration),
							 duration,
							 cycle);
		}
		
		void moveOscillate(COMMON::IFocalPoint@ focus, int duration, float xDist, float yDist, float frequency, float offset, bool cycle = false) {
			moveInterpolator(INTERPOLATION::PointPlusSine(focus,INTERPOLATION::getX,frequency,xDist,offset,duration),
							 INTERPOLATION::PointPlusSine(focus,INTERPOLATION::getY,frequency,yDist,offset,duration),
							 duration,
							 cycle);
		}
		
		void moveFollow(COMMON::IFocalPoint@ focus) {
			moveInterpolator(INTERPOLATION::Point(focus,INTERPOLATION::getX),
							 INTERPOLATION::Point(focus,INTERPOLATION::getY),
							 -1,
							 true);
		}
		
		void moveInterpolator(INTERPOLATION::Interpolator@ xInterpolatorMove, INTERPOLATION::Interpolator@ yInterpolatorMove, int duration, bool cycle = false) {
			moveTimer.start(duration);
			@this.xInterpolatorMove = @xInterpolatorMove;
			@this.yInterpolatorMove = @yInterpolatorMove;
			moveCycle = cycle;
		}
		
		bool move() {
			if(isActive && moveTimer.isStarted())
				if(moveTimer.elapsedTime() <= moveTimer.interval() || moveTimer.interval() == -1) {
					this.xPos = xInterpolatorMove.value(moveTimer.elapsedTime());
					this.yPos = yInterpolatorMove.value(moveTimer.elapsedTime());
					return true;
				} else if(moveCycle && moveTimer.elapsedTime() > moveTimer.interval()) {
					moveTimer.reset();
					this.xPos = xInterpolatorMove.value(moveTimer.elapsedTime());
					this.yPos = yInterpolatorMove.value(moveTimer.elapsedTime());
					return true;
				}
			return false;
		}
		
		bool isMoving() {
			return moveTimer.isStarted() && !moveTimer.isFinished();
		}
		
		void stopMove() {
			moveTimer.stop();
		}
	}
	
	interface IScalable {
		void scaleLinear(float scaleInit, float scaleEnd, int duration, bool cycle = false);
		void scaleAccelerate(float scaleInit, float scaleEnd, int duration, float acceleration, bool cycle = false);
		void scaleDeccelerate(float scaleInit, float scaleEnd, int duration, float decceleration, bool cycle = false);
		void scaleOscillate(float xMin, float xMax, float yMin, float yMax, int duration, float frequency, int offset, bool cycle = false);
		void scaleInterpolator(INTERPOLATION::Interpolator@ xInterpolator, INTERPOLATION::Interpolator@ yInterpolator, int duration, bool cycle = false);
		bool scale();
		bool isScaling();
		void stopScaling();
	}
	
	mixin class Scalable : IScalable {
		INTERPOLATION::Interpolator@ xInterpolatorScale;
		INTERPOLATION::Interpolator@ yInterpolatorScale;
		COMMON::Timer scaleTimer();
		bool scaleCycle;
	
		void scaleLinear(float scaleStart, float scaleEnd, int duration, bool cycle = false) {
			scaleInterpolator(INTERPOLATION::Linear(scaleStart,scaleEnd,duration),
							  INTERPOLATION::Linear(scaleStart,scaleEnd,duration),
							  duration,
							  cycle);

		}
		
		void scaleAccelerate(float scaleStart, float scaleEnd, int duration, float acceleration, bool cycle = false) {
			scaleInterpolator(INTERPOLATION::Exponential(scaleStart,scaleEnd,acceleration,duration),
							  INTERPOLATION::Exponential(scaleStart,scaleEnd,acceleration,duration),
							  duration,
							  cycle);
		}
		
		void scaleDeccelerate(float scaleStart, float scaleEnd, int duration, float decceleration, bool cycle = false) {
			scaleInterpolator(INTERPOLATION::Logarithmic(scaleStart,scaleEnd,decceleration,duration),
							  INTERPOLATION::Logarithmic(scaleStart,scaleEnd,decceleration,duration),
							  duration,
							  cycle);
		}
		
		void scaleOscillate(float xMin, float xMax, float yMin, float yMax, int duration, float frequency, int offset, bool cycle = false) {
			float xDist = (xMax-xMin)/2;
			float xMid  = xMin + xDist;
			float yDist = (yMax-yMin)/2;
			float yMid  = yMin + yDist;
			scaleInterpolator(INTERPOLATION::PointPlusSine(COMMON::Position(xMid,yMid),INTERPOLATION::getX,frequency,xDist,offset,duration),
							  INTERPOLATION::PointPlusSine(COMMON::Position(xMid,yMid),INTERPOLATION::getY,frequency,yDist,offset,duration),
							  duration,
							  cycle);
		}
		
		void scaleInterpolator(INTERPOLATION::Interpolator@ xInterpolator, INTERPOLATION::Interpolator@ yInterpolator, int duration, bool cycle = false) {
			scaleTimer.start(duration);
			@this.xInterpolatorScale = @xInterpolator;
			@this.yInterpolatorScale = @yInterpolator;
			scaleCycle = cycle;
		}
		
		bool scale() {
			if(isActive && scaleTimer.isStarted())
				if(scaleTimer.elapsedTime() <= scaleTimer.interval() || scaleTimer.interval() == -1) {
					this.spriteScaleX = xInterpolatorScale.value(scaleTimer.elapsedTime());
					this.spriteScaleY = yInterpolatorScale.value(scaleTimer.elapsedTime());
					return true;
				} else if(scaleCycle && scaleTimer.elapsedTime() > scaleTimer.interval()) {
					scaleTimer.reset();
					this.spriteScaleX = xInterpolatorScale.value(scaleTimer.elapsedTime());
					this.spriteScaleY = yInterpolatorScale.value(scaleTimer.elapsedTime());
					return true;
				}
			return false;
		}
		
		bool isScaling() {
			return scaleTimer.isStarted() && !scaleTimer.isFinished();
		}
		
		void stopScaling() {
			scaleTimer.stop();
		}
	}
	
	interface IRotatable {
		void rotate(float numRotations, int duration, int offset, bool clockwise, bool cycle = false);
		bool rotate();
		bool isRotating();
		void stopRotating();
	}
	
	mixin class Rotatable : IRotatable {
		INTERPOLATION::Interpolator@ interpolatorRotate;
		COMMON::Timer rotateTimer();
		bool rotateCycle;
	
		void rotate(float numRotations, int duration, int offset, bool clockwise, bool cycle = false) {
			float start, end, point = 1024*numRotations+offset;
			if(clockwise) {
				start = offset;
				end = point;
			} else {
				start = point;
				end = offset;
			}
			
			rotateInterpolator(INTERPOLATION::Linear(start,end,duration),
							   duration,
							   cycle);
		}
		
		void rotateInterpolator(INTERPOLATION::Interpolator@ interpolator, int duration, bool cycle = false) {
			rotateTimer.start(duration);
			@this.interpolatorRotate = interpolator;
			rotateCycle = cycle;
		}
		
		bool rotate() {
			if(isActive && rotateTimer.isStarted())
				if(rotateTimer.elapsedTime() <= rotateTimer.interval() || rotateTimer.interval() == -1) {
					this.spriteAngle = int(interpolatorRotate.value(rotateTimer.elapsedTime()));
					return true;
				} else if(rotateCycle && rotateTimer.elapsedTime() > rotateTimer.interval()) {
					rotateTimer.reset();
					this.spriteAngle = int(interpolatorRotate.value(rotateTimer.elapsedTime()));
					return true;
				}
			return false;
		}
		
		bool isRotating() {
			return rotateTimer.isStarted() && !rotateTimer.isFinished();
		}
		
		void stopRotating() {
			rotateTimer.stop();
		}
	}
	
	interface IColorable {
		void tintLinear(int startTint, int endTint, int duration, bool cycle = false);
		void tintOscillate(float min, float max, int duration, float frequency, int offset, bool cycle = false);
		void palshiftLinear(int startTint, int endTint, int duration, bool cycle = false);
		void palshiftOscillate(float min, float max, int duration, float frequency, int offset, bool cycle = false);
		bool color();
		bool isColoring();
		void stopColoring();
	}
	
	mixin class Colorable : IColorable {
		INTERPOLATION::Interpolator@ interpolatorColor;
		COMMON::Timer colorTimer();
		bool colorCycle;
		
		void tintLinear(int startTint, int endTint, int duration, bool cycle = false) {
			spriteMode = SPRITE::TINTED;
			colorInterpolator(INTERPOLATION::Linear(startTint,endTint,duration),
							  duration,
							  cycle);
		}
		
		void tintOscillate(float min, float max, int duration, float frequency, int offset, bool cycle = false) {
			float halfDist = (max-min)/2;
			float mid  = min + halfDist;
			spriteMode = SPRITE::TINTED;
			colorInterpolator(INTERPOLATION::PointPlusSine(COMMON::Position(mid,mid),INTERPOLATION::getX,frequency,halfDist,offset,duration),
							  duration,
							  cycle);
		}
		
		void palshiftLinear(int startTint, int endTint, int duration, bool cycle = false) {
			spriteMode = SPRITE::PALSHIFT;
			colorInterpolator(INTERPOLATION::Linear(startTint,endTint,duration),
							  duration,
							  cycle);
		}
		
		void palshiftOscillate(float min, float max, int duration, float frequency, int offset, bool cycle = false) {
			float dist = (min-max)/2;
			float mid  = min + dist;
			spriteMode = SPRITE::PALSHIFT;
			colorInterpolator(INTERPOLATION::PointPlusSine(COMMON::Position(mid,mid),INTERPOLATION::getX,frequency,dist,offset,duration),
							  duration,
							  cycle);
		}
		
		void colorInterpolator(INTERPOLATION::Interpolator@ interpolator, int duration, bool cycle = false) {
			colorTimer.start(duration);
			@this.interpolatorColor = interpolator;
			colorCycle = cycle;
		}
		
		bool color() {
			if(isActive && colorTimer.isStarted()) {
				if(colorTimer.elapsedTime() <= colorTimer.interval() || colorTimer.interval() == -1) {
					this.spriteParam = int(interpolatorColor.value(colorTimer.elapsedTime()));
					return true;
				} else if(colorCycle && colorTimer.elapsedTime() > colorTimer.interval()) {
					colorTimer.reset();
					this.spriteParam = int(interpolatorColor.value(colorTimer.elapsedTime()));
					return true;
				}
			}
			return false;
		}
		
		bool isColoring() {
			return colorTimer.isStarted() && !colorTimer.isFinished();
		}
		
		void stopColoring() {
			colorTimer.stop();
		}
	}
	
	interface IPatrollable {
		void patrolLinear(array<float>@ pathX, array<float>@ pathY, int duration, bool cycle = true);
		//void patrolSinusoidal(array<float>@ pathX, array<float>@ pathY, int duration, bool cycle = true,float frequency = 1, float amplitude=32, float offset=0);
		void patrolInterpolator(array<float>@ pathX, array<float>@ pathY, INTERPOLATION::PathInterpolator@ xInterpolator, INTERPOLATION::PathInterpolator@ yInterpolator, int duration, bool cycle = true);
		bool patrol();
		bool isPatrolling();
	}
	
	mixin class Patrollable : IPatrollable, IMovable {
		array<float>@ pathX;
		array<float>@ pathY;
		int duration;
		bool pathCycle = false;
		float pathTotalDist;
		float percentDist;
		int portionDuration;
		int pathIndex;
		COMMON::Timer patrolTimer();
		INTERPOLATION::PathInterpolator@ xInterpolator;
		INTERPOLATION::PathInterpolator@ yInterpolator;
		
		void patrolLinear(array<float>@ pathX, array<float>@ pathY, int duration, bool cycle = true) {
			patrolInterpolator(pathX,
							   pathY,
							   INTERPOLATION::Linear(pathX[0],pathX[1],portionDuration),
							   INTERPOLATION::Linear(pathY[0],pathY[1],portionDuration),
							   duration,
							   cycle);
		}
		
		/*void patrolSinusoidal(array<float>@ pathX, array<float>@ pathY, int duration, bool cycle = true,float frequency = 1, float amplitude=32, float offset=0) {
			initPatrol(pathX,pathY,duration,cycle);
			@this.xInterpolator = INTERPOLATION::LinePlusSine(pathX[0],pathX[1],duration,frequency,amplitude,offset);
			@this.yInterpolator = INTERPOLATION::LinePlusSine(pathY[0],pathY[1],duration,frequency,amplitude,offset);
			moveInterpolator(xInterpolator,yInterpolator,portionDuration);
		}*/
		
		void patrolInterpolator(array<float>@ pathX, array<float>@ pathY, INTERPOLATION::PathInterpolator@ xInterpolator, INTERPOLATION::PathInterpolator@ yInterpolator, int duration, bool cycle = true) {
			initPatrol(pathX,pathY,duration,cycle);
			@this.xInterpolator = @xInterpolator;
			@this.yInterpolator = @yInterpolator;
			moveInterpolator(xInterpolator,yInterpolator,portionDuration);
		}
		
		void initPatrol(array<float>@ pathX, array<float>@ pathY, int duration, bool cycle = true) {
			this.duration = duration;
			@this.pathX = @pathX;
			@this.pathY = @pathY;
			this.pathCycle = cycle;
			
			uint i = 0;
			pathTotalDist = 0;
			for(; i < pathX.length-1; i++) pathTotalDist += distance(pathX[i],pathY[i],pathX[i+1],pathY[i+1]);;
			if(pathCycle) pathTotalDist += distance(pathX[i],pathY[i],pathX[0],pathY[0]);
			this.pathIndex = 1;
			percentDist = distance(pathX[0],pathY[0],pathX[1],pathY[1]) / pathTotalDist;
			
			patrolTimer.start(duration);
			portionDuration = int(percentDist * duration);
		}
		
		bool patrol() {
			if(isActive && isPatrolling()) {
				if(!move()) {
					int oldIndex = pathIndex;
					pathIndex = (pathIndex + 1) % pathX.length();
					percentDist = distance(pathX[oldIndex],pathY[oldIndex],pathX[pathIndex],pathY[pathIndex]) / pathTotalDist;
					portionDuration = int(percentDist * duration);
					if(!pathCycle && pathIndex == 0) return true;
					xInterpolator.reset(pathX[oldIndex],pathX[pathIndex],portionDuration);
					yInterpolator.reset(pathY[oldIndex],pathY[pathIndex],portionDuration);
					moveInterpolator(xInterpolator,yInterpolator,portionDuration);
				}
				return true;
			}
			return false;
		}
		
		bool isPatrolling() {
			return pathCycle || (patrolTimer.isStarted() && !patrolTimer.isFinished());
		}
	}
	
	interface IWarpable {
		void warpIn(int warpColor = 90);
		void warpOut(int warpColor = 90);
		void warpTo(float xEnd, float yEnd, int warpColor = 90);
		bool warp();
		bool isWarping();
		bool isWarpingIn();
		bool isWarpingOut();
		bool isWarpedOut();
	}

	mixin class Warpable : IWarpable {
		COMMON::Timer warpTimer();
		bool warpInOrOut; //true=in,false=out
		int  warpAnim;
		int  warpFrameStart;
		int  warpColor;
		bool warpedOut = false;
		bool warpingTo = false;
		HANDLING::Bullet warpPrevBulletHandling;
		HANDLING::Player warpPrevPlayerHandling;
		float warpTargetX, warpTargetY;
		
		int WARP_FRAMES = 25;
		
		void warpIn(int warpColor = 90) {
			this.warpInOrOut = true;
			this.warpColor = warpColor;
			this.initWarp(this.warpColor, SOUND::COMMON_TELPORT2,76,0);
		}
		
		void warpOut(int warpColor = 90) {
			this.warpInOrOut = false;
			this.warpColor = warpColor;
			this.initWarp(this.warpColor, SOUND::COMMON_TELPORT1,75,2);
		}
		
		void warpTo(float xEnd, float yEnd, int warpColor = 90) {
			this.warpingTo = true;
			this.warpTargetX = xEnd;
			this.warpTargetY = yEnd;
			this.warpOut(warpColor);
		}
		
		void initWarp(int warpColor, SOUND::Sample sound,int anim,int frameStart) {
			this.stopWarpingHelper();
			this.warpAnim = anim;
			this.warpFrameStart = frameStart;
			this.warpColor = warpColor;
			if(!warpingTo && warpInOrOut) {
				/*this.warpPrevBulletHandling = this.bulletHandling;
				this.warpPrevPlayerHandling = this.playerHandling;
				this.bulletHandling = HANDLING::IGNOREBULLET;
				this.playerHandling = HANDLING::SPECIAL;*/
			}
			this.warpedOut = !this.warpInOrOut;
			warpTimer.start(WARP_FRAMES);
			jjSample(xPos, yPos, sound);
		}
		
		bool warp() {
			if(isActive && warpTimer.isStarted()) {
				if(warpTimer.elapsedTime() < WARP_FRAMES) {
					jjDrawSprite(xPos, yPos, ANIM::SPAZ,this.warpAnim, this.warpFrameStart+(warpTimer.elapsedTime()/5), this.direction, SPRITE::SINGLECOLOR, this.warpColor);
					return true;
				} else if(warpTimer.elapsedTime() == WARP_FRAMES) {
					if(warpingTo && !warpInOrOut) {
						xPos = warpTargetX;
						yPos = warpTargetY;
						warpIn(warpColor);
						return true;
					} else {
						stopWarping();
						return false;
					}
				}
			}
			return false;
		}
		
		void stopWarpingHelper() {
			if(warpTimer.elapsedTime() <= WARP_FRAMES) {
				/*this.bulletHandling = this.warpPrevBulletHandling;
				this.playerHandling = this.warpPrevPlayerHandling;*/
				this.warpTimer.stop();
			}
		}
		
		void stopWarping() {
			if(warpTimer.elapsedTime() <= WARP_FRAMES) {
				this.warpingTo = false;
			}
			stopWarpingHelper();
		}
		
		bool isWarping() {
			return warpTimer.isStarted() && !warpTimer.isFinished();
		}
		
		bool isWarpingIn() {
			return this.isWarping() && this.warpInOrOut;
		}
		
		bool isWarpingOut() {
			return this.isWarping() && !this.warpInOrOut;
		}
		
		bool isWarpedOut() {
			return this.warpedOut;
		}
	}
	
	interface IComposite {
		int addObject(CUSTOM::Object@ obj);
		void removeObject(CUSTOM::Object@ obj);
		void set_xPos(float xPos);
		void set_yPos(float yPos);
		void delete();
	}

	mixin class Composite : IComposite {
		array<CUSTOM::Object@> compositeObjects();
		
		int addObject(CUSTOM::Object@ obj) {
			int len = compositeObjects.length();
			compositeObjects.insertLast(obj);
			return len;
		}
		
		void removeObject(CUSTOM::Object@ obj) {
			int index = -1;
			for(uint i = 0; i < compositeObjects.length; i++) 
				if(compositeObjects[i] is obj) {
					index = i;
					break;
				}
			if(index >= 0)
				compositeObjects.removeAt(index);
		}
		
		void set_xPos(float xPos) {
			float diff = xPos - this.xPos;
			for(uint i = 0; i < compositeObjects.length(); i++) {
				if(compositeObjects[i].isActive)
					compositeObjects[i].xPos = compositeObjects[i].xPos + diff;
			}
			if(isActive)
				CUSTOM::Object::set_xPos(xPos);
		}
		
		void set_yPos(float yPos) {
			float diff = yPos - this.yPos;
			for(uint i = 0; i < compositeObjects.length(); i++) {
				if(compositeObjects[i].isActive)
					compositeObjects[i].yPos = compositeObjects[i].yPos + diff;
			}
			if(isActive)
				CUSTOM::Object::set_yPos(yPos);
		}
		
		void cleanup() {
			for(uint i = 0; i < compositeObjects.length(); i++) {
				if(compositeObjects[i].isActive)
					compositeObjects[i].delete();
			}
		}
	}
}

namespace GAME {
	funcdef CUSTOM::Object@ ENCOUNTER_STAGE();

	class Encounter : CUSTOM::Object {
		array<ENCOUNTER_STAGE@> stages;
		CUSTOM::Object@ curStage = null;
		bool startFromCurStage;
		bool replayIntro;
		int cur = -1;
		int max = -1;
		
		Encounter(array<ENCOUNTER_STAGE@> stages, bool startFromCurStage = true) {
			super(NIL,-32,-32);
			this.stages = stages;
			this.startFromCurStage = startFromCurStage;
			this.replayIntro = replayIntro;
			
			startNextStage();
		}
		
		void onDeath() {
			if(!this.isActive) return;
			if(curStage.isActive)
				curStage.delete();
			if(startFromCurStage) cur--;
			else cur = -1;
			startNextStage();
		}
		
		void startNextStage() {
			if(!this.isActive) return;
			if(curStage !is null && curStage.isActive) curStage.delete();
			if(int(cur++) >= int(stages.length()) || stages.length() == 0) {delete();return;}
			if(cur > max) max = cur;
			@curStage = @stages[cur]();
		}
		
		void behavior() {
			if(curStage !is null && !curStage.isActive)
				startNextStage();
		}
		
		void delete() {
			if(curStage !is null && curStage.isActive)
				curStage.delete();
			CUSTOM::Object::delete();
		}
		
		int stageNumber() {return cur;}
		
		CUSTOM::Object@ currentStage() {return curStage;}
	}
	
	class String : CUSTOM::Object {
		string txt;
		STRING::Size stringS;
		STRING::Mode stringM;
		SPRITE::Mode spriteM;
		int par;
		
		String(float xPos, float yPos, string text, STRING::Size stringSize = STRING::MEDIUM, STRING::Mode stringMode = STRING::NORMAL, SPRITE::Mode spriteMode = SPRITE::NORMAL, int param = 0) {
			super(NIL,xPos,yPos);
			this.txt = text;
			this.stringS = stringSize;
			this.stringM = stringMode;
			this.spriteM = spriteMode;
			this.par = param;
		}
		
		void set_text(string text) 	{this.txt = txt;}
		string get_text() 			{return this.txt;}
		
		void set_stringSize(STRING::Size stringSize) {this.stringS = stringSize;}
		STRING::Size get_stringSize()				 {return this.stringS;}
		
		void set_stringMode(STRING::Mode stringMode) {this.stringM = stringMode;}
		STRING::Mode get_stringMode() 				 {return this.stringM;}
		
		void set_spriteMode(SPRITE::Mode spriteMode) {this.spriteM = spriteMode;}
		SPRITE::Mode get_spriteMode()				 {return this.spriteM;}
		
		void set_param(int param) 					 {this.par = param;}
		int get_param() 							 {return this.par;}
		
		void behavior() {draw();}
		
		int draw() {
			if(spriteM != SPRITE::NORMAL)
				jjDrawString(xPos,yPos,txt,stringS,jjTEXTAPPEARANCE(stringM),param,spriteM);
			else
				jjDrawString(xPos,yPos,text,stringS,stringM,param,1);
			return -1;
		}
	}
	
	class FloatingString : String {
		COMMON::Timer timer();
		COMMON::IFocalPoint@ focus;
		float xOffset;
		
		FloatingString(string text, COMMON::IFocalPoint@ focus, float xOffset, float yOffset, int duration) {
			super(focus.xPos+xOffset,focus.yPos+yOffset,text,STRING::SMALL);
			this.xOffset = xOffset;
			@this.focus = @focus;
			this.timer.start(duration);
		}
		
		void behavior() {
			if(!focus.isActive || timer.isFinished()) {delete(); return;}
			xPos = focus.xPos + xOffset;
			if(jjGameTicks & 1 == 0) yPos = yPos - 1;
			draw();
		}
	}
	
	class Speech {
		COMMON::IFocalPoint@ focus;
		string text;
		int textWidth;
		
		Speech(COMMON::IFocalPoint@ focus, string text, int textWidth) {
			@this.focus = @focus; this.text = text; this.textWidth = textWidth;
		}
		
		COMMON::IFocalPoint@ getPos() { return focus; }
		string getText() { return text; }
		int getTextWidth() { return textWidth; }
	}
	
	class Dialogue : CUSTOM::Object {
		array<GAME::Speech@> dialogue;
		int index = -1;
		bool endOnDeath;
		
		Dialogue(array<GAME::Speech@> dialogue, bool endOnDeath = true) {
			super(NIL,-32,-32);
			this.dialogue = dialogue;
			this.endOnDeath = endOnDeath;
		}
		
		void showNext() {
			if(endOnDeath) {
				for(uint i = 0; i < dialogue.length(); i++) {	
					if(!dialogue[i].getPos().isActive) {
						delete();
						return;
					}

				}
			}
			if(uint(++index) < dialogue.length()) {
				GAME::Speech@ speech = @dialogue[index];
				if(speech.getPos().isActive) {
					GAME::FloatingString(speech.getText(),
										 speech.getPos(),
										 -speech.getTextWidth()/2,
										 -32,
										 getDelay());
										}
			} else
				delete();
		}
		
		bool displayNextMessage() {
			return false; //Override me
		}
		
		int getDelay() {
			return 0;
		}
	
		void behavior() {
			if(displayNextMessage()) showNext();
		}
	}
	
	class DialogueFixedDelay : Dialogue {
		uint delay;
		COMMON::Timer timer();
		
		DialogueFixedDelay(array<GAME::Speech@> dialogue, int delay, bool endOnDeath = true) {
			super(dialogue,endOnDeath);
			this.delay = delay;
		}
		
		bool displayNextMessage() {
			bool display = false;
			if(!timer.isStarted() || timer.isFinished()) {
				display = true;
				this.timer.start(delay);
			}
			return display;
		}
		
		int getDelay() {
			return delay;
		}
	}
	
	class DialogueScaledDelay : Dialogue {
		COMMON::Timer timer();
		int delay;
		int minDelay;
		DialogueScaledDelay(array<GAME::Speech@> dialogue, int minDelay = 0, bool endOnDeath = true) {
			super(dialogue,endOnDeath);
			this.minDelay = minDelay;
		}
		
		bool displayNextMessage() {
			bool display = false;
			if(!timer.isStarted() || timer.isFinished()) {
				display = true;
				if(uint(index+1) < dialogue.length()) {
					delay = dialogue[index+1].getText().length()*7;
					delay = delay < minDelay ? minDelay : delay;
					this.timer.start(delay);
				}
			}
			return display;
		}
		
		int getDelay() {
			return delay;
		}
	}
	
	class Camera : CUSTOM::Object {
		jjPLAYER@ play;
		
		Camera(jjPLAYER@ player) {
			super(NIL,player.xPos,player.yPos);
			@this.play = @player;
		}
		
		float get_xPos() const { return play.cameraX+10*32;}
		float get_yPos() const { return play.cameraY+7.5*32;}
		
		void unfreeze() {
			play.cameraUnfreeze();
		}
		
		void behavior() {
			if(isMoving() && moveTimer.elapsedTime() > 1) {
				play.cameraFreeze(this.jjObject.xPos,this.jjObject.yPos,true,true);
			}
		}
	}
	
	interface IBossMeter : COMMON::IFocalPoint, MIXIN::IMovable, MIXIN::IPatrollable {
		void set_maxEnergy(int maxEnergy);
		int get_maxEnergy();
		
		void set_energy(int energy);
		int get_energy() const;
		
		void delete();
	}
	
	class AbstractBossMeter : CUSTOM::Object, IBossMeter {
		int health;
		int maxHealth;
		COMMON::IFocalPoint@ focus;
		
		int BAR_WIDTH = 136;
		int BAR_OFFSET_Y = 17;
		int BAR_HEIGHT = 4;
		int BAR_COLOR = 0;
	
		AbstractBossMeter(COMMON::IFocalPoint@ focus) {
			super(NIL, focus.xPos, focus.yPos);
			@this.focus = @focus;
			determineCurAnim(ANIM::BOSS,0);
		}
		
		void set_maxEnergy(int maxEnergy) 	{this.maxHealth = maxEnergy;}
		int get_maxEnergy() 				{return maxHealth;}
		
		void set_energy(int energy)			{if(energy > maxHealth) this.health = maxHealth;
											 else if(energy > 0) this.health = energy;
											 else this.health = 0;}
		int get_energy() const				{return this.health;}
	}
	
	class BossMeterUI : AbstractBossMeter, CUSTOM::IInterfaceElement {
		BossMeterUI(float xPos, float yPos) {
			super(COMMON::Position(xPos,yPos));
		}
		
		//TODO: Sux that we need two different drawing functions.
		//Maybe the object draw function should take a canvas somehow?
		void behavior() {}
		int draw(bool useCustomSpriteProperties = true) const { return -1;}
		
		void draw(jjPLAYER@ player, jjCANVAS@ canvas) {
			float percentHealth = float(health)/float(maxHealth);
			float xBar = int(xPos-(BAR_WIDTH/2)+BAR_WIDTH*percentHealth);
			float yBar = yPos+BAR_OFFSET_Y;
			int lenBar = int(xPos+(BAR_WIDTH/2)-xBar);
			
			canvas.drawSprite(int(xPos),int(yPos),ANIM::BOSS,0,0,0);
			canvas.drawSprite(int(xPos),int(yPos),ANIM::BOSS,0,1,0);
			canvas.drawRectangle(int(xBar),int(yBar),lenBar,BAR_HEIGHT,BAR_COLOR);
		}
		
		void delete() {
			CUSTOM::Object::delete();
			CUSTOM::UI.removeElement(this);
		}
	}
	
	class BossMeterFloating : AbstractBossMeter {
		int yOffset;
		BossMeterFloating(COMMON::IFocalPoint@ focus, int yOffset = 0) {
			super(focus);
			this.yOffset = yOffset;
		}
		
		void behavior() {
			if(!focus.isActive) delete();
			xPos = focus.xPos;
			yPos = focus.yPos+yOffset;
			draw();
		}
		
		int draw(bool useCustomSpriteProperties = true) const {
			float percentHealth = float(health)/float(maxHealth);
			float xBar = int(xPos-(BAR_WIDTH/2)+BAR_WIDTH*percentHealth);
			float yBar = yPos+BAR_OFFSET_Y;
			int lenBar = int(xPos+(BAR_WIDTH/2)-xBar);
			
			jjDrawSprite(xPos,yPos,ANIM::BOSS,0,0,0);
			jjDrawSprite(xPos,yPos,ANIM::BOSS,0,1,0);
			jjDrawRectangle(xBar,yBar,lenBar,BAR_HEIGHT,BAR_COLOR);
			return -1;
		}
	}
}

//--------------------------------------------------------
//END OF SaLLiB
//--------------------------------------------------------

//TODO: Make Particle class
//TODO: Replace patrol with general interpolation chaining
//TODO: Builder style set methods
//TODO: Allow camera/objects to be created globally
//TODO: Consider removing set_xPos/yPos from MIXIN::Composite (since I override them in pileofcheese)
//TODO: Angle offset conventions.
//TODO: Documentation

//TODO: Consider composable interpolations

//TODO: Make boss meter work with multiple local players
//TODO: call delete or some kind of cleanup on death - bug violet even more about delete hook

//TODO: Write full-fledged player class
//TODO: Consider interpolateVar function handle to replace move(), rotate(), scale() etc. Can we make the interpolation mixins more abstract?