Downloads containing kangaroo-custom.asc

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Original Levels - Stone... ShaddowBlack0 Single player 7 Download file

File preview

#pragma require "kangaroo.j2a"
//#pragma require "Anims2.j2a"
#pragma require "BL18.asc"
#pragma require "HH17.asc"
#pragma require "hh17_icicle.j2a"

#pragma require "Resize v10.asc"
#include "Resize v10.asc"

#pragma require "BL18.j2a"
#pragma require "BL18.bmp"
#pragma require "HPYeahoo.wav"
#pragma require "HPCrystal.wav"
#pragma require "HPSwitch.wav"
#pragma require "HPTreasure.wav"
#pragma require "HPTrigger.wav"
#pragma require "HPWarp.wav"
#pragma require "ESJM1.wav"
#pragma require "ESJM2.wav"
#pragma require "ESJM3.wav"
#pragma require "ESJF1.wav"
#pragma require "ESJF2.wav"
#pragma require "ESJF3.wav"
/*
API (all expected to be called in onLevelLoad):
	jjOBJ@ Kangaroo::MakeEventJoey(uint8 eventID, int minX = 0, int maxX = 4, int minY = 6, int maxY = 12, int jumpDelay = 35, int minDistance = 224)
		Assign a specific event slot to the Joey enemy, e.g. replacing another enemy type or a food object or something. If you assign multiple event slots, you can get Joey enemies with different parameters.
	jjOBJ@ Kangaroo::MakeEventJill(uint8 eventID, uint8 spawn = 0, bool secondStage = false, int textID = -1)
		Assign a specific event slot to the Jill boss.
			"spawn", if non-zero, is an eventID that the boss will occasionally create from her pouch. This is assumed to be a Joey enemy but may be other objects as well.
			The "secondStage" bool causes Jill to turn red and jump faster after she has lost three-quarters of her health.
			If "textID" is 0-16, defeating Jill will display that text ID.
	void Kangaroo::OnJillDefeat(JILLCALLBACKFUNC@ callback = null)
		Three seconds after defeating Jill, this function will be called. The JILLCALLBACKFUNC pattern is the same as the behavior pattern: a void-returning function taking a jjOBJ@ as its only argument. If "callback" is left null, or if OnJillDefeat is never called, defeating a Jill will simply end the level.
	void Kangaroo::Joey(jjOBJ@ obj)
		The behavior function for the Joey enemy
	void Kangaroo::Jill(jjOBJ@ obj)
		The behavior function for the Jill boss
*/


namespace Kangaroo {

class Point {
	float x; float y;
	Point(){}
	Point(float xx, float yy) {
		x = xx;
		y = yy;
	}
	const Point& opAssign(const Point &in other) {
		x = other.x;
		y = other.y;
		return this;
	}
	bool opEquals(const Point &in other) const {
		return x == other.x && y == other.y;
	}
}
enum CollectibleTypes {
	Coin, Pink,Turquoise,Red,Silver,Blue,Purple,Gold,Green
};
class CollectibleLocation {
	Point Location;
	bool Found = false;
	CollectibleTypes Type;
	CollectibleLocation(Point l, CollectibleTypes t) { Location = l; Type = t; }
	CollectibleLocation(){}
}

class SortableDistance {
	float distance;
	uint index;
	SortableDistance(){}
	SortableDistance(float d, uint i) { distance = d; index = i; }
	int opCmp(const SortableDistance &in other) const {
		return int(distance - other.distance);
	}
}
Point CoinWarpLocation;
bool CoinWarpFound = false;
int ArrowTimer = 0;
const uint ArrowSprite = jjAnimations[jjAnimSets[ANIM::FLAG]];
array<CollectibleLocation> CollectibleLocations;
void ShowArrow() {
	if (!CoinWarpFound) {
		uint coinsTotal = 0;
		uint coinsFound = 0;
		uint collectiblesFound = 0;
		int keyX = 56;
		const int keyY = jjSubscreenHeight - 19;
		for (uint i = 0; i < CollectibleLocations.length; ++i) {
			const auto@ location = CollectibleLocations[i];
			const bool found = location.Found;
			
			/*if (found)
				collectiblesFound += 1;
			if (location.Type == CollectibleTypes::Coin) {
				if (found)
					coinsFound += 1;
				coinsTotal += 1;
			} else {
				canvas.drawSpriteFromCurFrame(keyX += 20, keyY, KeyFrame, 1, found ? SPRITE::SINGLEHUE : SPRITE::SHADOW, KeyColors[location.Type - 1]);
			}*/
		}
	}
	
	
	if (CoinWarpFound)
		return;
	if (ArrowTimer > jjGameTicks + 70*4)
		ArrowTimer = jjGameTicks;
	else if (ArrowTimer < jjGameTicks) {
		if (ArrowTimer + 64 >= jjGameTicks) {
			const Point playerLocation(Player.xPos, Player.yPos);
			array<SortableDistance> distances;
			for (uint i = 0; i < CollectibleLocations.length; ++i) {
				if (CollectibleLocations[i].Found)
					continue;
				const float deltaX = abs(playerLocation.x - CollectibleLocations[i].Location.x);
				const float deltaY = abs(playerLocation.y - CollectibleLocations[i].Location.y);
				distances.insertLast(SortableDistance(deltaX * deltaX + deltaY * deltaY, i));
			}
			const Point@ target = null;
			if (distances.length == 0) {
				@target = CoinWarpLocation;
			} else {
				distances.sortAsc();
				if (distances[0].distance > 300*300)
					@target = CollectibleLocations[distances[0].index].Location;
			}
			if (target !is null) {
				const int angle = GetAngle(target.x - playerLocation.x, playerLocation.y - target.y);
				const float scale = (112 - (jjSin((jjGameTicks - ArrowTimer) << 3) * -64)) / 128.f;
				jjDrawRotatedSpriteFromCurFrame(
					Player.xPos + jjCos(angle) * 32, Player.yPos + jjSin(angle) * 32,
					ArrowSprite, 970 - angle, scale, scale, SPRITE::SINGLEHUE, 72, -9
				);
			}
		} else {
			ArrowTimer = jjGameTicks + 70*3;
		}
	}
}

	namespace Private {
		void jillDefeatedDefaultAction(jjOBJ@) {
			jjNxt();
		}
		JILLCALLBACKFUNC@ jillCallback = jillDefeatedDefaultAction;
		
		bool animsLoaded = false;
		uint customAnimID = 0;
		void loadAnims() {
			if (!animsLoaded) {
				animsLoaded = true;
				while (jjAnimSets[ANIM::CUSTOM[customAnimID]] != 0)
					++customAnimID;
				customAnimID = ANIM::CUSTOM[customAnimID];
				jjAnimSets[customAnimID].load(0, "kangaroo.j2a");
				if (!jjSampleIsLoaded(SOUND::BUBBA_BUBBABOUNCE1))
					jjAnimSets[ANIM::BUBBA].load();
			}
		}
		
		void applyGenericEnemySettingsToPreset(jjOBJ@ preset) {
			preset.playerHandling = HANDLING::ENEMY;
			preset.bulletHandling = HANDLING::HURTBYBULLET;
			preset.causesRicochet = false;
			preset.isBlastable = false;
			preset.triggersTNT = true;
			preset.isFreezable = true;
			preset.isTarget = true;
			preset.scriptedCollisions = false;
			preset.direction = 1;
			preset.freeze = 0;
		}
		
		void putKangarooOnGround(jjOBJ@ obj, int width, int height) {
			while (!jjMaskedHLine(int(obj.xPos) - width/2, width, int(obj.yPos) + height/2))
				obj.yPos += 1;
		}
		
		uint firstGloveAnimationFrame;
		const jjANIMFRAME@ roundExplosionFrame = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::AMMO] + 5] + 2];;
		
		void doGloveAt(int x, int y) {
			for (int i = 0; i < jjLocalPlayerCount; ++i) {
				jjPLAYER@ localPlayer = jjLocalPlayers[i];
				if (localPlayer.blink == 0 && roundExplosionFrame.doesCollide(x, y, 0, jjAnimFrames[localPlayer.curFrame], int(localPlayer.xPos), int(localPlayer.yPos), localPlayer.direction))
					localPlayer.hurt();
			}
			for (int i = jjObjectCount - 1; i > 0; --i) {
				jjOBJ@ obj = jjObjects[i];
				if (obj.playerHandling == HANDLING::PLAYERBULLET && obj.state != STATE::EXPLODE && roundExplosionFrame.doesCollide(x, y, 0, jjAnimFrames[obj.curFrame], int(obj.xPos), int(obj.yPos), obj.direction)) {
					obj.ricochet();
					//obj.playerHandling = HANDLING::ENEMYBULLET;
				}
			}
		}
	}
	enum KangarooVariables {
		kvWIDTH = 0, kvHEIGHT, kvMINX, kvMAXX, kvMINY, kvMAXY, kvJUMPDELAY, kvMINDISTANCE, kvGLOVE1FRAME, kvGLOVE2FRAME, kvSECONDSTAGE
	}
	jjOBJ@ MakeEventJoey(uint8 eventID, int minX = 0, int maxX = 4, int minY = 6, int maxY = 12, int jumpDelay = 35, int minDistance = 224) {
		Kangaroo::Private::loadAnims();
		
		jjOBJ@ preset = jjObjectPresets[eventID];
		preset.behavior = Joey;
		preset.determineCurAnim(Kangaroo::Private::customAnimID, 0);
		preset.frameID = 0; preset.determineCurFrame();
		
		Kangaroo::Private::applyGenericEnemySettingsToPreset(preset);
		
		preset.deactivates = true;
		preset.energy = 1;
		preset.points = 200;
		preset.yAcc = 0.33f;
		preset.counter = 0;
		preset.var[kvWIDTH] = 12;
		preset.var[kvHEIGHT] = 28;
		preset.var[kvMINX] = minX;
		preset.var[kvMAXX] = maxX;
		preset.var[kvMINY] = minY;
		preset.var[kvMAXY] = maxY;
		preset.var[kvJUMPDELAY] = jumpDelay;
		preset.var[kvMINDISTANCE] = minDistance;
		
		return preset;
	}
	
	funcdef void JILLCALLBACKFUNC(jjOBJ@);
	jjOBJ@ MakeEventJill(uint8 eventID, uint8 spawn = 0, bool secondStage = false, int textID = -1) {
		Kangaroo::Private::loadAnims();
		if (jjAnimSets[ANIM::GLOVE] == 0)
			jjAnimSets[ANIM::GLOVE].load();
		Kangaroo::Private::firstGloveAnimationFrame = jjAnimations[jjAnimSets[ANIM::GLOVE] + 3];
		
		jjOBJ@ preset = jjObjectPresets[eventID];
		preset.behavior = Jill;
		preset.determineCurAnim(Kangaroo::Private::customAnimID, 1);
		preset.frameID = 0; preset.determineCurFrame();
		
		Kangaroo::Private::applyGenericEnemySettingsToPreset(preset);
		
		preset.doesHurt = spawn;
		preset.yAcc = 0.16f;
		preset.energy = 100;
		preset.points = 5000;
		preset.counterEnd = 210; //death wait
		preset.special = textID;
		preset.playerHandling = HANDLING::DYING; //no initial collision damage
		preset.var[kvWIDTH] = 32;
		preset.var[kvHEIGHT] = 98;
		preset.var[kvMINX] = 2;
		preset.var[kvMAXX] = 4;
		preset.var[kvMINY] = 5;
		preset.var[kvMAXY] = 10;
		preset.var[kvJUMPDELAY] = 140;
		preset.var[kvMINDISTANCE] = 400;
		preset.var[kvGLOVE1FRAME] = 0;
		preset.var[kvGLOVE2FRAME] = 0;
		preset.var[kvSECONDSTAGE] = secondStage ? 1 : 0;
		
		return preset;
	}
	void OnJillDefeat(JILLCALLBACKFUNC@ callback = null) {
		if (callback !is null)
			@Kangaroo::Private::jillCallback = callback;
	}
	
	
	void Joey(jjOBJ@ obj) {
		const int width = obj.var[kvWIDTH];
		const int height = obj.var[kvHEIGHT];
		switch (obj.state) {
			case STATE::START:
				obj.state = STATE::IDLE;
				Kangaroo::Private::putKangarooOnGround(obj, width, height);
			case STATE::IDLE:
				if (obj.counter == 0 || --obj.counter == 0) {
					const int nearestPlayerID = obj.findNearestPlayer(int(pow(obj.var[kvMINDISTANCE], 2)));
					if (nearestPlayerID >= 0) {
						jjPLAYER@ nearestPlayer = jjPlayers[nearestPlayerID];
						obj.xSpeed = (nearestPlayer.xPos - obj.xPos) / 20.0f;
						obj.direction = (obj.xSpeed >= 0) ? 1 : -1;
						
						float xSpeed = abs(obj.xSpeed);
						if (xSpeed > obj.var[kvMAXX]) xSpeed = obj.var[kvMAXX];
						else if (xSpeed < obj.var[kvMINX]) xSpeed = obj.var[kvMINX];
						obj.xSpeed = xSpeed * obj.direction;
						
						float ySpeed = abs((nearestPlayer.yPos - obj.yPos) / 20.0f);
						if (ySpeed > obj.var[kvMAXY]) ySpeed = obj.var[kvMAXY];
						else if (ySpeed < obj.var[kvMINY]) ySpeed = obj.var[kvMINY];
						obj.ySpeed = -ySpeed;
						
						obj.state = STATE::JUMP;
						obj.counter = obj.var[kvJUMPDELAY];
						jjSample(obj.xPos, obj.yPos, ((jjRandom() & 1) == 0) ? SOUND::BUBBA_BUBBABOUNCE1 : SOUND::BUBBA_BUBBABOUNCE2);
					} else
						obj.direction = (obj.xPos > jjLocalPlayers[0].xPos) ? -1 : 1;
				}
				break;
			case STATE::FREEZE:
				if (obj.freeze > 0)
					--obj.freeze;
				if (obj.freeze <= 0) {
					obj.state = obj.oldState;
					obj.unfreeze(0);
				}
				break;
			case STATE::JUMP:{
				obj.yPos += (obj.ySpeed += obj.yAcc);
				const int newXPos = int(obj.xPos + obj.xSpeed) + (width * obj.direction)/2;
				if ((newXPos < 0) || (newXPos > jjLayerWidth[4]*32) || jjMaskedVLine(newXPos, int(obj.yPos - height/2), height)) {
					obj.xSpeed = -obj.xSpeed;
					obj.direction = -obj.direction;
				} 
				obj.xPos += obj.xSpeed;
				int newYPos = int(obj.yPos + obj.ySpeed);
				if (obj.ySpeed < 0) {
					if ((newYPos < 0) || jjMaskedHLine(int(obj.xPos) - width/2, width, newYPos - height/2)) {
						obj.ySpeed = obj.yAcc;
						obj.frameID = 2;
					} else obj.frameID = 1;
				}
				if (obj.ySpeed > 0) {
					if ((newYPos > jjLayerHeight[4]*32) || jjMaskedHLine(int(obj.xPos) - width/2, width, newYPos + height/2)) {
						obj.state = STATE::IDLE;
						obj.ySpeed = 0;
						obj.frameID = 0;
						Kangaroo::Private::putKangarooOnGround(obj, width, height);
					} else obj.frameID = 2;
				}
				obj.determineCurFrame();
				break;
			} case STATE::DEACTIVATE:
				obj.deactivate();
				return;
			case STATE::KILL:
				obj.delete();
				return;
		}
		obj.draw();
	}
	void Jill(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::START:
				obj.state = STATE::DELAYEDSTART;
			case STATE::DELAYEDSTART:
				for (int i = 0; i < jjLocalPlayerCount; ++i) {
					jjPLAYER@ localPlayer = jjLocalPlayers[i];
					if (localPlayer.bossActivated) {
						localPlayer.boss = obj.objectID;
						obj.state = STATE::START;
					}
				}
				if (obj.state == STATE::START) {
					obj.playerHandling = HANDLING::ENEMY;
					break;
				}
				return;
			case STATE::KILL:
				if (obj.special >= 0) //textID
					jjLocalPlayers[0].showText(obj.special, 0);
				obj.playerHandling = HANDLING::DYING;
				obj.state = STATE::DONE;
			case STATE::DONE:
				if (--obj.counterEnd == 0) {
					obj.delete();
					Kangaroo::Private::jillCallback(obj);
				}
				return;
			default:
				break;
		}
		
		int oldState = obj.state;
		obj.behave(Joey, false);
		obj.frameID = 0;
		obj.determineCurFrame();
		
		const int direction = obj.direction;
		const bool secondStage = (obj.var[kvSECONDSTAGE] != 0 && obj.energy < 25);
		if (secondStage) {
			if (obj.var[kvSECONDSTAGE] == 1) {
				obj.var[kvSECONDSTAGE] = 2;
				obj.var[kvMINX] = 3;
				obj.var[kvMAXX] = 5;
				obj.var[kvJUMPDELAY] = 50;
				obj.var[kvMINDISTANCE] = 600;
			}
		}
		if (obj.doesHurt != 0 && (jjRandom() & 255) == 0) {
			jjOBJ@ spawn = jjObjects[jjAddObject(obj.doesHurt, obj.xPos, obj.yPos + 11, obj.objectID, CREATOR::OBJECT, BEHAVIOR::INACTIVE)];
			jjBEHAVIOR originalBehavior = jjObjectPresets[obj.doesHurt].behavior;
			if (originalBehavior == Joey) {
				spawn.direction = obj.direction;
				spawn.xSpeed = spawn.direction * (1 + (jjRandom() & 3));
				spawn.ySpeed = -6;
				spawn.state = STATE::JUMP;
			}
			spawn.behavior = originalBehavior;
		}
		
		SPRITE::Mode mode = SPRITE::NORMAL;
		uint8 param = 15;
		SPRITE::Mode modeDark = SPRITE::BRIGHTNESS;
		uint8 paramDark = 96;
		if (obj.justHit != 0) {
			mode = modeDark = SPRITE::SINGLECOLOR;
			paramDark = param;
			const int nearestPlayerID = obj.findNearestPlayer(64);
			for (int i = 0; i < jjLocalPlayerCount; ++i) {
				jjPLAYER@ localPlayer = jjLocalPlayers[i];
				if (localPlayer.specialMove != 0) {
					localPlayer.specialMove = 0;
					localPlayer.ySpeed -= 1;
					localPlayer.extendInvincibility(-35);
				}
			}
			
		} else if (obj.state == STATE::FREEZE) {
			mode = modeDark = SPRITE::FROZEN;
		} else if (secondStage) {
			mode = modeDark = SPRITE::TINTED;
			param = 25;
			paramDark = 29;
		}
		
		int armAngle = obj.age;
		if (obj.state != STATE::FREEZE) {
			const int nearestPlayerID = obj.findNearestPlayer(1000000);
			if (nearestPlayerID >= 0) {
				jjPLAYER@ nearestPlayer = jjPlayers[nearestPlayerID];
				const float deltaX = nearestPlayer.xPos - obj.xPos;
				const float deltaY = nearestPlayer.yPos - obj.yPos;
				armAngle = int(atan2(
				    (direction == 1) ? deltaY : -deltaY,
				    abs(deltaX)
				) * -512.0 * 0.318309886142228);
			}
			obj.age = armAngle;
		}
		const float armSin = jjSin(armAngle);
		const float armCos = jjCos(armAngle);
		int tailAngle = int(obj.ySpeed*-16)*direction;
		const int tailX = int(obj.xPos) - 32 * direction;
		const int tailY = int(obj.yPos) + 23;
		const int gloveLength = (12 + 29 + 29) * obj.direction;
		const int legAngle = int(obj.ySpeed*8)*direction;
		const int arm1X = int(obj.xPos) - 2 * direction;
		const int arm1Y = int(obj.yPos) - 7;
		const int arm2X = int(obj.xPos) + 4 * direction;
		const int arm2Y = int(obj.yPos - 11);
		//if (tailAngle > 0) tailAngle = 0;
		
		if (obj.ySpeed < 0) {
			obj.animSpeed = jjSampleLooped(obj.xPos, obj.yPos, SOUND::COMMON_FLAMER, obj.animSpeed);
			if (obj.state != STATE::FREEZE && (jjRandom() & 1) == 0) {
				jjPARTICLE@ part = jjAddParticle(PARTICLE::FIRE);
				if (part !is null) {
					part.xSpeed = jjSin(tailAngle) * 2;
					part.ySpeed = jjCos(tailAngle) * 2;
					part.xPos = tailX + part.xSpeed * 8;
					part.yPos = tailY + part.ySpeed * 8;
					part.xSpeed += abs(obj.xSpeed) * -obj.direction;
				}
			}
			if (oldState == STATE::IDLE) {
				obj.var[kvGLOVE1FRAME] = 1;
				jjSample(obj.xPos, obj.yPos, SOUND::COMMON_PISTOL1);
				obj.var[kvGLOVE2FRAME] = 0;
			}
		}
		{
			int oldGloveFrame = obj.var[kvGLOVE1FRAME];
			if (oldGloveFrame > 0 && oldGloveFrame < 12 && (jjGameTicks & 3) == 1)
				obj.var[kvGLOVE1FRAME] = oldGloveFrame + 1;
			
			oldGloveFrame = obj.var[kvGLOVE2FRAME];
			if (obj.state == STATE::IDLE && oldGloveFrame == 0) {
				obj.var[kvGLOVE2FRAME] = 1;
				jjSample(obj.xPos, obj.yPos, SOUND::COMMON_PISTOL1);
			}
			if (oldGloveFrame > 0 && oldGloveFrame < 12 && (jjGameTicks & 3) == 1)
				obj.var[kvGLOVE2FRAME] = oldGloveFrame + 1;
		}
		
		const int glove2FrameID = Kangaroo::Private::firstGloveAnimationFrame + (obj.var[kvGLOVE2FRAME] + 3) % 12;
		jjDrawRotatedSpriteFromCurFrame(
			arm2X + 12*armSin + gloveLength*armCos,
			arm2Y + 12*armCos - gloveLength*armSin,
			 glove2FrameID,
			armAngle - 256 * obj.direction,
			direction, 2, modeDark, paramDark
		); //glove
		if (obj.state != STATE::FREEZE) {
			const int glove2Length = gloveLength + (jjAnimFrames[glove2FrameID].height - 30) * 2 * obj.direction;
			Kangaroo::Private::doGloveAt(
				int(arm2X + 12*armSin + glove2Length*armCos),
				int(arm2Y + 12*armCos - glove2Length*armSin)
			);
		}
		jjDrawRotatedSpriteFromCurFrame(arm2X, arm2Y, obj.curFrame + 1, armAngle, direction, 1, modeDark, paramDark); //back arm
		
		jjDrawRotatedSpriteFromCurFrame(obj.xPos - 24 * direction, obj.yPos + 24, obj.curFrame + 2, legAngle, direction, 1, modeDark, paramDark); //back leg
		jjDrawRotatedSpriteFromCurFrame(tailX, tailY, obj.curFrame + 3, tailAngle, direction, 1, mode, param); //tail
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, direction, mode, param); //body
		jjDrawRotatedSpriteFromCurFrame(obj.xPos - 30 * direction, obj.yPos + 28, obj.curFrame + 2, legAngle, direction, 1, mode, param); //leg
		
		jjDrawRotatedSpriteFromCurFrame(arm1X, arm1Y, obj.curFrame + 1, armAngle, direction, 1, mode, param); //arm
		const int glove1FrameID = Kangaroo::Private::firstGloveAnimationFrame + (obj.var[kvGLOVE1FRAME] + 3) % 12;
		jjDrawRotatedSpriteFromCurFrame(
			arm1X + 12*armSin + gloveLength*armCos,
			arm1Y + 12*armCos - gloveLength*armSin,
			 glove1FrameID,
			armAngle - 256 * obj.direction,
			direction, 2, mode, param
		); //glove
		if (obj.state != STATE::FREEZE) {
			const int glove1Length = gloveLength + (jjAnimFrames[glove1FrameID].height - 30) * 2 * obj.direction;
			Kangaroo::Private::doGloveAt(
				int(arm2X + 12*armSin + glove1Length*armCos),
				int(arm2Y + 12*armCos - glove1Length*armSin)
			);
		}
	}
//}
//namespace EBL18{
	bool IsMurderer = false;
	bool allowSpecialMoves = true;
	bool monsterRemain = true;
	jjPLAYER@ Player = jjLocalPlayers[0];
	
	void mRemain(bool remain){
		monsterRemain = remain;
	}
	void makeRed(jjOBJ@ o){
		//o.curFrame = (jjAnimations[o.curAnim] = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::Misc]] + 8]).firstFrame;
		//const uint KeyFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::Misc]] + 8];
		//jjAnimSets[ANIM::SUCKER] + 4
		const uint KeyFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[9]] + 4];
		o.curFrame = KeyFrame;//(jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::Misc]] + 8]).firstFrame;
		//Recolor(jjAnimations[jjAnimSets[ANIM::SUCKER] + 4], array<uint8>={24}, RecolorReplace(40)); //red to orange
		//Recolor(jjAnimations[jjAnimSets[ANIM::SUCKER] + 4], array<uint8>={80}, RecolorReplace(24)); //green to red
	}
	array<uint> GemTargets(4, 0), GemCounts(4, 0);
	int CountGems(){
		int countGem = GemTargets[0]+GemTargets[1]+GemTargets[2]+GemTargets[3];
		return countGem;
	}
	void Setup(){
		jjAnimSets[ANIM::CUSTOM[AnimSets::Misc]].load(AnimSets::Misc, "BL18.j2a");
		jjAnimSets[ANIM::CUSTOM[9]].load(99, "Anims.j2a"); //was Anims2.j2a
		//const uint firstMiscMiscFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::Misc]] + 1];
		//KeyFrame = firstMiscMiscFrame + 3;
		array<bool> EventsFound(256, false);
		/*for (int l = 1; l < jjObjectCount; l++) {
			jjOBJ@ o = jjObjects[l];
			if(o.eventID == OBJECT::FLOATSUCKER) {
				o.behavior = WhiteSucker();
			}
		}*/
		for (int i = 0; i < jjLayerHeight[4]; i++) {
			for (int j = 0; j < jjLayerWidth[4]; j++) {
				uint8 eventID = jjEventGet(j, i);
				//detect events hidden inside walls
				if (eventID == OBJECT::BANANA) //HocusWall
					eventID = jjParameterGet(j, i, 5, 8);
				else if (eventID == OBJECT::CHERRY) // HocusDestructScenery
					eventID = jjParameterGet(j, i, 0, 8);
				if (eventID == OBJECT::REDGEM || eventID == OBJECT::GREENGEM || eventID == OBJECT::BLUEGEM || eventID == OBJECT::PURPLEGEM) {
					GemTargets[eventID - OBJECT::REDGEM] += 1;
				}
				if (eventID > 32 && !EventsFound[eventID]) {
					EventsFound[eventID] = true;
					jjOBJ@ preset = jjObjectPresets[eventID];
					/*if (jjEventGet(j, i) == OBJECT::FATCHICK) {
						FatChick(preset);
					}else if (jjEventGet(j, i) == OBJECT::FENCER) {
						Fencer(preset);
					}else if (jjEventGet(j, i) == OBJECT::TUBETURTLE) {
						TubeTurle(preset);
					}else if (jjEventGet(j, i) == OBJECT::FLOATLIZARD) {
						//FloatLizard(preset);
					}else if (jjEventGet(j, i) == OBJECT::BAT) {
						RedBat(preset);
					}else if (jjEventGet(j, i) == OBJECT::MONKEY || jjEventGet(j, i) == OBJECT::STANDMONKEY) {
						Monkey(preset);
					}else if (jjEventGet(j, i) == OBJECT::DOGGYDOGG) {
						Muttshroom(preset);
					}else if (jjEventGet(j, i) == OBJECT::WATERMELON) {
						Trogdor(preset);
					}*/
					
					switch (eventID) {
						//case OBJECT::SILVERCOIN:
						//	EventsFound[eventID] = false;
						//	CollectibleLocations.insertLast(CollectibleLocation(Point(i * 32 + 15, j * 32 + 15), CollectibleTypes(jjParameterGet(i, j, 0, 4))));
						//	break;
						case OBJECT::FATCHICK:
							FatChick(preset);
							break;
						case OBJECT::FENCER:
							Fencer(preset);
							break;
						case OBJECT::LIZARD:
							//FloatLizard(preset);
							break;
						case OBJECT::TUBETURTLE:
							TubeTurle(preset);
							break;
						case OBJECT::FISH:
							//Fish(preset);
							break;
						case OBJECT::BAT:
							//RedBat(preset);
							break;
						case OBJECT::MONKEY:
						case OBJECT::STANDMONKEY:
							Monkey(preset);
							break;
						case OBJECT::DOGGYDOGG:
							Muttshroom(preset);
							break;
						case OBJECT::WATERMELON:
							Trogdor(preset);
							break;
						case OBJECT::BILSY:
							//MiniBilsy(preset);
							break;
						case OBJECT::PEACH:
							//FrogFly(jjObjectPresets[OBJECT::DRAGONFLY]);
							//Frog(preset);
							break;
						case OBJECT::RAVEN:
							Phoenix(preset);
							break;
						case OBJECT::DEMON:
							Demon(preset);
							break;
						case OBJECT::SPARK:
							//Duet(preset);
							break;
						case OBJECT::HATTER:
							Hatter(preset);
							break;
						case OBJECT::FLOATSUCKER:
							//FloatSucker(preset);
							//jjObjectPresets[OBJECT::FLOATSUCKER].behavior = WhiteSucker();
							Recolor(jjAnimations[jjAnimSets[ANIM::SUCKER] + 4], array<uint8>={24}, RecolorReplace(40)); //red to orange
							Recolor(jjAnimations[jjAnimSets[ANIM::SUCKER] + 4], array<uint8>={80}, RecolorReplace(24)); //green to red
							jjSampleLoad(SOUND::P2_FART, "HH17_balloon.wav");
							break;
						case OBJECT::SUCKER:
							Sucker(preset);
							jjObjectPresets[OBJECT::FLOATSUCKER].behavior = WhiteSucker();
							break;
						case OBJECT::DRAGON:
							//jjObjectPresets[OBJECT::DRAGON].behavior = IceDragon();
							break;
						case OBJECT::SKELETON:
							jjObjectPresets[OBJECT::SKELETON].behavior = BoneChucker();
							break;
						case OBJECT::CRAB:
							jjObjectPresets[OBJECT::CRAB].behavior = YetiCrab();
							jjAnimSets[ANIM::UTERUS].load();
							break;
						case OBJECT::DRAGONFLY:
							jjObjectPresets[OBJECT::DRAGONFLY].behavior = DragonFly();
							break;
					}
						/*case OBJECT::SILVERCOIN:
							EventsFound[eventID] = false;
							CollectibleLocations.insertLast(CollectibleLocation(Point(x * 32 + 15, y * 32 + 15), CollectibleTypes(jjParameterGet(x, y, 0, 4))));
							break;
						case OBJECT::WARP:
							if (jjParameterGet(x, y, 17, 1) == 0) { //ShowAnim=0: just an area, not a warp object
								EventsFound[eventID] = false;
								break;
							}
							CoinWarpLocation = Point(x * 32, y * 32);
							preset.behavior = CoinWarp;
							break;
						case OBJECT::CARROT:
							preset.behavior = RestrictedCarrot;
							break;
						case OBJECT::BOUNCERAMMO3:
						case OBJECT::ICEAMMO3:
						case OBJECT::SEEKERAMMO3:
						case OBJECT::RFAMMO3:
						case OBJECT::TOASTERAMMO3:
						case OBJECT::TNTAMMO3:
						case OBJECT::GUN8AMMO3:
						case OBJECT::GUN9AMMO3:
							preset.behavior = NotOnHardPickup;
							break;
						case OBJECT::APPLE:
							preset.scriptedCollisions = true;
							preset.curFrame = KeyFrame + 1;
							preset.behavior = Lock();
							break;
						case OBJECT::BANANA:
							preset.behavior = HocusWall;
							preset.playerHandling = HANDLING::PARTICLE;
							break;
						case OBJECT::CHERRY:
							preset.behavior = HocusDestructScenery();
							preset.playerHandling = HANDLING::SPECIAL;
							preset.bulletHandling = HANDLING::DETECTBULLET;
							preset.scriptedCollisions = true;
							preset.isFreezable = false;
							preset.triggersTNT = true;
							preset.determineCurAnim(ANIM::DESTSCEN, 2);
							preset.determineCurFrame();
							break;
						case OBJECT::REDGEM:
							HocusGem(preset, 0, 0, 10, 100);
							break;
						case OBJECT::PURPLEGEM:
							HocusGem(preset, 0, 3, 4, 250);
							break;
						case OBJECT::GREENGEM:
							HocusGem(preset, 1, 1, 5, 500);
							break;
						case OBJECT::BLUEGEM:
							HocusGem(preset, 2, 2, 5, 1000);
							break;
						case OBJECT::ORANGE: {
							preset.behavior = WarpPotion();
							preset.scriptedCollisions = true;
							Recolor(jjAnimations[preset.determineCurAnim(ANIM::PICKUPS, 78)], array<uint8>={88}, RecolorReplace(32));
							break; }
						case OBJECT::PEAR:
							preset.behavior = Switch();
							preset.playerHandling = HANDLING::PARTICLE;
							preset.curFrame = firstMiscMiscFrame + 5;
							break;
						case OBJECT::PRETZEL:
							preset.behavior = Friend();
							preset.playerHandling = HANDLING::PARTICLE;
							preset.determineCurAnim(ANIM::MENU,0,false);
							preset.curFrame = firstMiscMiscFrame + 5;
							break;
						case OBJECT::STRAWBERRY:
							preset.behavior = Furryball;
							preset.playerHandling = HANDLING::ENEMYBULLET;
							preset.animSpeed = jjDifficulty > 0 ? 2 : 1;
							preset.curFrame = firstMiscMiscFrame + 9;
							preset.killAnim = jjAnimSets[ANIM::AMMO] + 5;
							break;
						case OBJECT::LEMON:
							preset.behavior = Elevator;
							preset.isBlastable = false;
							preset.playerHandling = HANDLING::SPECIAL;
							preset.bulletHandling = HANDLING::DESTROYBULLET;
							preset.curFrame = skullFrame + TrueColor::NumberOfFramesPerImage;
							preset.special = firstMiscMiscFrame + 10; //8-bit
							preset.deactivates = false;
							break;
						case OBJECT::LIME:
							preset.behavior = Spikes;
							preset.playerHandling = HANDLING::ENEMYBULLET;
							preset.animSpeed = 1;
							preset.curFrame = firstMiscMiscFrame + 12;
							break;
						case 122: //lava
							LevelContainsLava = true;
							preset.behavior = function(obj) { obj.behavior = Lava(); };
							preset.playerHandling = HANDLING::PARTICLE;
							preset.determineCurAnim(ANIM::CUSTOM[AnimSets::Misc], 6);
							preset.special = firstMiscMiscFrame + 14;
							break;
						case OBJECT::BRIDGE:
							jjAnimSets[ANIM::BRIDGE].load(AnimSets::Bridge, "BL18.j2a");
							break;
						case OBJECT::SAVEPOST:
							preset.deactivates = false;
							preset.behavior = CheckpointWrapper;
							preset.curFrame = (jjAnimations[preset.curAnim] = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::Misc]] + 8]).firstFrame;
							break;
						case OBJECT::BIGROCK:
							preset.behavior = CompanionRock;
							jjAnimFrames[preset.curFrame].hotSpotX -= 4; //to be better able to avoid landing on elevators
							break;
						case OBJECT::GRAPES:
							preset.behavior = Balloon();
							preset.scriptedCollisions = true;
							preset.curFrame = firstMiscMiscFrame + 18;
							break;
						case OBJECT::FLYCARROT:
							preset.behavior = TemporaryFlyCarrot(); //this is going to be limited to the PJ level, but it's similar enough code to some of this other stuff I find it more convenient to have it in here
							preset.scriptedCollisions = true;
							break;
						case OBJECT::LETTUCE:
							if (jjDifficulty > 0) { //no smart bombs on easy
								preset.behavior = SmartBomb();
								preset.playerHandling = HANDLING::SPECIAL;
								preset.scriptedCollisions = true;
								preset.curAnim = jjAnimSets[TrueColor::FindCustomAnim()].load(AnimSets::SmartBomb, "BL18.j2a");
								preset.killAnim = jjAnimSets[ANIM::AMMO] + 77;
								preset.lightType = LIGHT::BRIGHT;
								preset.light = 9;
							} else {
								preset.behavior = BEHAVIOR::INACTIVE;
							}
							break;
						case OBJECT::BURGER:
							preset.behavior = Gear;
							preset.playerHandling = HANDLING::PARTICLE;
							preset.curAnim = jjAnimSets[TrueColor::FindCustomAnim()].load(AnimSets::Gear, "BL18.j2a");
							preset.determineCurFrame();
							break;
					}*/
				}
			}
		}
	}

class IceDragon : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::DRAGON, true);
		//obj.behave(BEHAVIOR::DRAGON, obj.var[1] == 0? true:false);
		if (obj.var[1] == 1) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos + 2, obj.curFrame, obj.direction, SPRITE::NORMAL, 3, 3, 3);
		obj.freeze = 0;
		for (int i = 1; i < jjObjectCount; i++) {
			if (jjObjects[i].behavior == BEHAVIOR::PLATFORM) {
				float dx = jjObjects[i].xOrg - obj.xOrg, dy = jjObjects[i].yOrg - obj.yOrg;
				if (dx * dx + dy * dy < 128 * 128) {
					obj.xPos = jjObjects[i].xPos;
					obj.yPos = jjObjects[i].yPos - 10;
					obj.var[1] = 1;
				}
			}
		}
		
		const float wantx = Player.xPos;
		const float wanty = Player.yPos - 96;
		if (jjGameTicks % 70 == 1) {
			IceBullet temp;
			jjOBJ@ icebullet = jjObjects[jjAddObject(OBJECT::ICEBULLETPU, int(obj.xPos), int(obj.yPos), obj.objectID, CREATOR::OBJECT, jjVOIDFUNCOBJ(temp.onBehave))];
			jjSample(icebullet.xPos, icebullet.yPos, SOUND::AMMO_ICEPU3);
			icebullet.xPos = obj.xOrg + (8 * obj.direction);
			icebullet.counterEnd = 30;
			icebullet.state = STATE::FLY;
			icebullet.playerHandling = HANDLING::ENEMYBULLET;
		}
	}
}

class NormalSucker : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::FLOATSUCKER, false);
		//obj.behave(BEHAVIOR::FLOATSUCKER, obj.state == STATE::KILL? true:false);
		if (obj.state == STATE::FLOAT) makeRed(obj);
			//jjDrawResizedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, (1 + (obj.age/20)) * obj.direction, (1 + (obj.age/20)), obj.freeze > 0? SPRITE::FROZEN : SPRITE::NORMAL); 
			jjDrawResizedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 1, 1, obj.freeze > 0? SPRITE::FROZEN : SPRITE::NORMAL); 
		int playerID = obj.findNearestPlayer(28000);
		/*if (playerID > -1 && obj.state == STATE::FLOAT) {
			if (obj.age < 100 && obj.freeze == 0) {
				obj.age += 5;
			}
		} else {
			if (obj.age > 0) obj.age -= 5;
		}
		
		if (obj.age == 5 && obj.var[1] == 1) {
			obj.var[0] = 1;
		}
		
		if (obj.age == 10) {
			obj.var[1] = 0;
		}
		
		if (obj.age == 0) {
			obj.var[0] = 0;
			obj.var[1] = 1;
		}*/
		
		if (obj.state != STATE::DONE) {
			//obj.behave(BEHAVIOR::FLOATSUCKER, false);
			if (obj.freeze == 0) {
				if (--obj.age == -175) {
					obj.age = (false ? 75 : 200) + (jjRandom() & 31);
					obj.lightType = LIGHT::NONE;
				} else if (obj.age < 0) {
					
				}
			}
		}else{
			
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bull, jjPLAYER@ play, int force) {
		if (bull !is null) {
			if (bull.playerHandling == HANDLING::PLAYERBULLET) {
				obj.energy -= bull.animSpeed;
				obj.justHit = 5;
				if (bull.var[3] == 3) obj.special = 350;
			}
			//obj.behavior = BEHAVIOR::SUCKER;
			//obj.state = STATE::EXPLODE;
			//obj.behavior = BEHAVIOR::FLOATSUCKER;
		}
		return true;
	}
}
/*
class WhiteSucker : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::FLOATSUCKER, obj.state == STATE::KILL? true:false);
		if (obj.state != STATE::KILL) jjDrawResizedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, (1 + (obj.age/20)) * obj.direction, (1 + (obj.age/20)), obj.freeze > 0? SPRITE::FROZEN : SPRITE::NORMAL); 
		int playerID = obj.findNearestPlayer(28000);
		if (playerID > -1 && obj.state == STATE::FLOAT) {
			if (obj.age < 100 && obj.freeze == 0) {
				obj.age += 5;
			}
		} else {
			if (obj.age > 0) obj.age -= 5;
		}
		
		if (obj.age == 5 && obj.var[1] == 1) {
			obj.var[0] = 1;
		}
		
		if (obj.age == 10) {
			obj.var[1] = 0;
		}
		
		if (obj.age == 0) {
			obj.var[0] = 0;
			obj.var[1] = 1;
		}
		
		float dx = jjPlayers[playerID].xPos - obj.xPos, dy = jjPlayers[playerID].yPos - obj.yPos;
		if (obj.freeze == 0 && dx * dx + dy * dy < obj.age * obj.age) {
			jjPlayers[playerID].hurt(1, false);
			if (jjPlayers[playerID].xPos > int(obj.xPos + 8)) jjPlayers[playerID].xSpeed = 4;
			else if (jjPlayers[playerID].xPos < int(obj.xPos - 8)) jjPlayers[playerID].xSpeed = -4;
			
			if (jjPlayers[playerID].xPos < int(obj.xPos + 8) && jjPlayers[playerID].xPos > int(obj.xPos - 8)) {
				jjPlayers[playerID].ySpeed = jjPlayers[playerID].yPos > obj.yPos? 3:-3;
				jjPlayers[playerID].xSpeed = 4 * obj.direction;
			} else jjPlayers[playerID].ySpeed = jjPlayers[playerID].yPos > obj.yPos? 1:-1;
		}
		
		if (obj.age == 5 && obj.var[1] == 1 && obj.freeze == 0) jjSample(obj.xPos, obj.yPos, SOUND::P2_FART, 48, 0);
		
		for (int i = 0; i < jjObjectCount; i++) {
			jjOBJ@ bull = jjObjects[i];
			if (bull.playerHandling == HANDLING::PLAYERBULLET && obj.state == STATE::FLOAT) {
				float bdx = bull.xPos - obj.xPos, bdy = bull.yPos - obj.yPos;
				if (bdx * bdx + bdy * bdy < 200 * 200) {
					if (obj.age < 100 && obj.freeze == 0) obj.age += 5;
				}
				if (bdx * bdx + bdy * bdy < obj.age * obj.age) {
					if (bull.var[3] == 3) {
						jjOBJ@ freeze = jjObjects[jjAddObject(OBJECT::ICEBULLET, obj.xPos, obj.yPos, obj.creatorID, CREATOR::PLAYER)];
						freeze.counterEnd = 1;
						freeze.state = STATE::FLY;
						freeze.xSpeed = 1;
						freeze.playerHandling = HANDLING::PLAYERBULLET;
						bull.delete();
					} else {
						bull.ricochet();
					}
				}
			}
		}
		
		if (obj.state == STATE::FREEZE) {
			obj.special--;
			if (obj.special == 0) {
				obj.unfreeze(1);
				obj.state = obj.oldState;
			}
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bull, jjPLAYER@ play, int force) {
		if (bull !is null) {
			if (bull.playerHandling == HANDLING::PLAYERBULLET) {
				obj.energy -= bull.animSpeed;
				obj.justHit = 5;
				if (bull.var[3] == 3) obj.special = 350;
			}
		}
		return true;
	}
}*/

class WhiteSucker : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::FLOATSUCKER, false);
		
		jjDrawResizedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 1, 1, obj.freeze > 0? SPRITE::FROZEN : SPRITE::NORMAL); 
		int playerID = obj.findNearestPlayer(28000);
		
		if (obj.state != STATE::DONE) {
			//obj.behave(BEHAVIOR::FLOATSUCKER, false);
			if (obj.freeze == 0) {
				if (--obj.age == -175) {
					obj.age = (false ? 75 : 200) + (jjRandom() & 31);
					obj.lightType = LIGHT::NONE;
				} else if (obj.age < 0) {
					//obj.behave(BEHAVIOR::FLOATSUCKER, false);
					//makeSparks(obj, true);
					bool reducedY = true;
					obj.lightType = LIGHT::BRIGHT;
					obj.doesHurt = jjSampleLooped(obj.xPos,obj.yPos,SOUND::COMMON_SHIELD_ELEC,obj.doesHurt);
					if (jjGameTicks & 3 == 0) {
						jjOBJ@ spark = jjObjects[jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos, obj.objectID)];
						spark.determineCurAnim(ANIM::PICKUPS, 85);
						spark.animSpeed = 1;
						spark.playerHandling = HANDLING::ENEMYBULLET;
						spark.xSpeed /= 7;
						if (reducedY)
							spark.ySpeed /= 2;
						else
							spark.ySpeed = -2.5;
						spark.lightType = LIGHT::POINT2;
					}
				}
			}
		}
		
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bull, jjPLAYER@ play, int force) {
		if (bull !is null) {
			if (bull.playerHandling == HANDLING::PLAYERBULLET) {
				obj.energy -= bull.animSpeed;
				obj.justHit = 5;
				if (bull.var[3] == 3) obj.special = 350;
			}
			//obj.behavior = BEHAVIOR::SUCKER;
			//obj.state = STATE::EXPLODE;
			//obj.behavior = BEHAVIOR::FLOATSUCKER;
		}
		return true;
	}
}

	const uint heartFrame = jjAnimations[jjAnimSets[ANIM::PICKUPS] + 41];
	enum AnimSets {
		Basketball, Bridge, Canopy, Cow, Duet, Egg, FatChick, Font, Fooruman, Gear, LavaLamp, Misc, Monk, Muttshroom, PurpleDragon, SmartBomb, Trogdor, TubeTurle,
		_LAST
	};
	enum Gender { Male, Female, Random };
	class Enemy : jjBEHAVIORINTERFACE {
		ANIM::Set animSetID;
		Gender gender = Gender::Random;
		Enemy(jjOBJ@ preset, Kangaroo::AnimSets asi = Kangaroo::AnimSets::_LAST) {
			preset.behavior = this;
			if (asi != AnimSets::_LAST)
				jjAnimSets[animSetID = TrueColor::FindCustomAnim()].load(asi, "BL18.j2a");
			preset.playerHandling = HANDLING::SPECIAL; //here special
			preset.scriptedCollisions = true;
			preset.bulletHandling = HANDLING::DETECTBULLET; //here special
			preset.isTarget = true;
			preset.triggersTNT = true;
			preset.isFreezable = true;
			preset.isBlastable = false; //not bothering with that nonsense
			
		}
		void die(jjOBJ@ obj, bool diedToBullet, bool diedWhileFrozen) const {
			Player.food += 1;
			obj.particlePixelExplosion(diedToBullet ? 0 : 2);
/*~			for (uint i = 0; i < 30; ++i) {
				if (i < 10)
					jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos, i, CREATOR::OBJECT, BloodSpatter);
				jjObjects[jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos - (jjRandom() & 15), i, CREATOR::OBJECT, BoneWrapper)].freeze = diedWhileFrozen ? 255 : 0;
			}*/
			/*if(obj.eventID == OBJECT::FLOATSUCKER){
				obj.state = STATE::KILL;
				removeFromEventMap(obj);
				obj.delete();
				jjOBJ@ newObj = jjObjects[jjAddObject(OBJECT::FLOATSUCKER, obj.xPos, obj.yPos, OBJECT::FLOATSUCKER)];//, CREATOR::OBJECT, WhiteSucker);
				//newObj.behavior = WhiteSucker();
				//newObj.energy = 5;
				//newObj.behavior = BEHAVIOR::FLOATSUCKER;
				//jjSampleLoad(SOUND::P2_FART, "HH17_balloon.wav");
				//jjObjects[jjAddObject(OBJECT::SUCKER, obj.xPos, obj.yPos)].behavior = BEHAVIOR::SUCKER;
				//newObj.state = STATE::FALL;
				//Sucker(newObj);
				//obj.energy = 0;
				
				//jjSamplePriority(jjRandom() & 3 < 3 ? SOUND::BAT_BATFLY1 : SOUND::BAT_BATFLY1);
				//jjSample(obj.xPos, obj.yPos, SOUND::SPAZSOUNDS_BURP, 63, 0); //SUCKER_FART actually
			}else{*/
				obj.energy = 0;
				obj.state = STATE::KILL;
				removeFromEventMap(obj);
				if(allowSpecialMoves == false)
					jjSamplePriority(jjRandom() & 3 < 3 ? SOUND::INTRO_BOEM1 : SOUND::INTRO_BOEM2);
				obj.delete();
			//}
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
			if (obj.state == STATE::START || obj.state == STATE::DELAYEDSTART)
				return true;
			const bool isProbablyBoss = jjObjectPresets[obj.eventID].energy >= 20;
			const bool objfreeze = obj.freeze != 0;
			if (bullet is null) {
				if (force != 0) {
					if (objfreeze) {
						obj.state = obj.oldState;
						obj.unfreeze(1);
					}
					if ((obj.energy -= 4) <= 0) {
						obj.energy = 0;
						if (!IsMurderer) {
							if(allowSpecialMoves == false){
								player.showText("#||||~@@MURDERER", STRING::LARGE);
								die(obj, false, objfreeze);
							}
						}
					} else {
						obj.justHit = 5;
					}
					if (!IsMurderer) {
						if(allowSpecialMoves == false){
							IsMurderer = true;
							jjPalette.fill(jjPALCOLOR(), 0.15);
							jjPalette.apply();
							if (!jjMusicLoad("DARKSIDE.MOD")) jjMusicStop();
						}
					}
//~					jjSample(obj.xPos, obj.yPos, ButtstompSounds[jjRandom() % ButtstompSounds.length]);
					if (force > 0) { //buttstomp or sugar rush
						player.buttstomp = 50; //landing
						player.ySpeed = player.ySpeed / -2 - 8;
						player.yAcc = 0;
						player.extendInvincibility(-70);
					} else if (force == -101) { //running into frozen enemy
						player.xAcc = 0;
						player.xSpeed /= -2;
						player.ySpeed = -6;
						player.extendInvincibility(-10);
					}
					if (jjObjectPresets[obj.eventID].energy > 20)
						player.specialMove = 0;
						
					if (!IsMurderer && allowSpecialMoves && obj.eventID != OBJECT::BAT && obj.eventID != OBJECT::FLOATSUCKER && monsterRemain) {
						obj.oldState = obj.state;
						obj.state = STATE::DONE;
						obj.playerHandling = HANDLING::SPECIALDONE;
						obj.deactivates = false;
						obj.triggersTNT = false;
						obj.isTarget = false;
						obj.lightType = LIGHT::NONE;
//~						if (!jjLowDetail)
//~							jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos, 0, CREATOR::OBJECT, Orgasm);
						const uint rand = jjRandom();
//~						jjSample(obj.xPos, obj.yPos, SOUND::Sample(((gender == Gender::Female || (gender == Gender::Random && (rand & 128) == 128)) ? ESJF : ESJM) + (rand % 3)));
//~						Player.lives += 1;
						removeFromEventMap(obj);
					}else if(obj.eventID == OBJECT::BAT || obj.eventID == OBJECT::FLOATSUCKER || !monsterRemain){
						die(obj, true, objfreeze);
					}
				} else if (!objfreeze)
					player.hurt();
			} else {
				if (objfreeze) {
					obj.state = obj.oldState;
					obj.unfreeze(0);
					force += 1;
				}
				if ((bullet.var[6] & 16) == 0) { //fireball
					bullet.state = STATE::EXPLODE;
				} else if (isProbablyBoss) {
					bullet.state = STATE::EXPLODE;
					force *= 2;
				}
				if ((obj.energy -= force) <= 0) {
					if (!IsMurderer && obj.eventID != OBJECT::BAT && obj.eventID != OBJECT::FLOATSUCKER && monsterRemain) {
						obj.oldState = obj.state;
						obj.state = STATE::DONE;
						obj.playerHandling = HANDLING::SPECIALDONE;
						obj.deactivates = false;
						obj.triggersTNT = false;
						obj.isTarget = false;
						obj.lightType = LIGHT::NONE;
//~						if (!jjLowDetail)
//~							jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos, 0, CREATOR::OBJECT, Orgasm);
						const uint rand = jjRandom();
//~						jjSample(obj.xPos, obj.yPos, SOUND::Sample(((gender == Gender::Female || (gender == Gender::Random && (rand & 128) == 128)) ? ESJF : ESJM) + (rand % 3)));
//~						Player.lives += 1;
						removeFromEventMap(obj);
					} else
						if(allowSpecialMoves == false || obj.eventID == OBJECT::BAT || obj.eventID == OBJECT::FLOATSUCKER || !monsterRemain){
							die(obj, true, objfreeze);
						}
				} else {
					obj.justHit = 5;
				}
			}
			if (obj.energy <= 0 && !isProbablyBoss) { 
				const auto rand = jjRandom();
				if (rand & 15 == 0) {
					jjOBJ@ pickup = jjObjects[jjAddObject(rand & 16 == 16 ? OBJECT::FASTFIRE : OBJECT::CARROT, obj.xPos, obj.yPos, obj.objectID,CREATOR::OBJECT, BEHAVIOR::FLICKERGEM)];
					pickup.playerHandling = HANDLING::DELAYEDPICKUP;
					pickup.var[2] = 20;
					pickup.counter = 400 - (jjDifficulty * 50);
					pickup.light = 1;
					pickup.lightType = LIGHT::LASER;
				}
			}
			return true;
		}
		void onBehave(jjOBJ@ obj) {}
		
		uint8 Layer = 4;
		void drawBodySprite(const jjOBJ@ obj, float xPos, float yPos, int curFrame, int angle = 0, float scale = 1) const {
			const SPRITE::Mode mode = obj.state == STATE::DONE ? SPRITE::TINTED : obj.state == STATE::FREEZE ? SPRITE::FROZEN : obj.justHit == 0 ? SPRITE::NORMAL : SPRITE::SINGLECOLOR;
			if (angle == 0)
				jjDrawSpriteFromCurFrame(xPos, yPos, curFrame, obj.direction, mode, 48, Layer);
			else
				jjDrawRotatedSpriteFromCurFrame(xPos, yPos, curFrame, angle, obj.direction * scale, scale, mode, 48, Layer);
		}
		void drawHearts(const jjOBJ@ obj, int curFrame = -1) const {
			if (obj.state == STATE::DONE) {
				if (curFrame < 0) curFrame = obj.curFrame;
				const float heartsY = obj.yPos + jjAnimFrames[obj.curFrame].hotSpotY - 8;
				for (int i = 0; i < 1024; i += 205) {
					const int heartAngle = (jjGameTicks << 3) + i;
					jjDrawResizedSpriteFromCurFrame(obj.xPos + jjSin(heartAngle) * 13, heartsY + jjCos(heartAngle) * 6, heartFrame, 0.5, 0.5, SPRITE::TINTED, 48, Layer);
				}
			}
		}
		void drawMainSprite(const jjOBJ@ obj, int curFrame = -1, int angle = 0) const {
			if (curFrame < 0) curFrame = obj.curFrame;
			drawHearts(obj, curFrame);
			drawBodySprite(obj, obj.xPos, obj.yPos, curFrame, angle);
		}
		void onDraw(jjOBJ@ obj) { drawMainSprite(obj); }
	}
	
	void removeFromEventMap(jjOBJ@ obj) {
		obj.eventID = 0; //prevent STATE::DEACTIVATE snafus
		jjEventSet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0);
	}
	
	class FatChick : Enemy {
		FatChick(jjOBJ@ preset) {
			super(preset, AnimSets::FatChick);
			gender = Gender::Female;
		}
		void onBehave(jjOBJ@ obj) override {
			if (obj.counter >= 70*2 - 1 && obj.state == STATE::WALK) {
				if (obj.counter++ == 70*2 - 1) {
					jjObjects[jjAddObject(OBJECT::BULLET, obj.xPos + 5*obj.direction, obj.yPos - 14, obj.objectID, CREATOR::OBJECT, FatHead)].curFrame = jjAnimations[jjAnimSets[animSetID]];
				} else if (obj.counter == 70*3)
					obj.counter = 0;
			} else {
				obj.behave(BEHAVIOR::FATCHICK, false);
			}
		}
		void onDraw(jjOBJ@ obj) override {
			drawMainSprite(obj, (obj.counter >= 70*2) ? jjAnimations[jjAnimSets[ANIM::FATCHK]] + 10 + ((obj.counter >> 2) & 1) : -1);
		}
	}
	
	int GetAngle(float xSpeed, float ySpeed) {
		return int(atan2(
		    ySpeed,
		    xSpeed
		) * -512.0 * 0.318309886142228);
	}
	
	void FatHead(jjOBJ@ obj) {
		//loop this and select the closest local player 
		//but be careful to hurt all
		
		if (obj.state == STATE::START) {
			obj.direction = jjObjects[obj.creatorID].direction;
			
			obj.special = GetAngle(Player.yPos - obj.yPos, obj.xPos - Player.xPos);
			obj.doesHurt = (jjRandom() & 3) + 6;
			obj.state = STATE::FLY;
			obj.playerHandling = HANDLING::ENEMYBULLET;
			obj.animSpeed = 1;
		} else if (obj.state == STATE::EXPLODE || ++obj.counter >= (!IsMurderer ? 120 + jjDifficulty * 40 : 255)) {
			obj.delete();
		} else {
			int angle = obj.special;
			float xPos = obj.xPos, yPos = obj.yPos;
			const jjANIMFRAME@ myFrame = jjAnimFrames[obj.curFrame];
			for (int i = 0; i < obj.counter; ++i) {
				jjDrawSpriteFromCurFrame(xPos, yPos, obj.curFrame, obj.direction, SPRITE::NORMAL,0, 3);
				if (Player.blink == 0 && myFrame.doesCollide(int(xPos), int(yPos), obj.direction, jjAnimFrames[Player.curFrame], int(Player.xPos), int(Player.yPos), Player.direction)) {
					Player.objectHit(obj, 0, HANDLING::ENEMYBULLET);
					//break;
				}
				xPos += jjSin(angle) * 2;
				yPos += jjCos(angle) * 2;
				angle += ((((((i >> 5) + 1) >> 1) & 1) << 1) - 1) * obj.doesHurt * obj.direction;
			}
		}	
	}
	
	
	void Recolor(jjANIMFRAME@ frame, const array<uint8> &in pattern, const array<uint8> &in replace) {
		jjPIXELMAP image(frame);
		for (int x = image.width - 1; x >= 0; --x)
			for (int y = image.height - 1; y >= 0; --y) {
				const uint8 color = image[x,y];
				if (pattern.find( color & ~7) >= 0)
					image[x,y] = replace[color & 7];
			}
		image.save(frame);
	}
	void Recolor(const jjANIMATION@ anim, const array<uint8> &in pattern, const array<uint8> &in replace) {
		for (uint frameID = 0; frameID < anim.frameCount; ++frameID)
			Recolor(jjAnimFrames[anim + frameID], pattern, replace);
	}
	array<uint8>@ RecolorReplace(uint8 hue) {
		return array<uint8> = {hue,hue+1,hue+2,hue+3,hue+4,hue+5,hue+6,hue+7};
	}
	
	int getParameterAtOrigin(const jjOBJ@ obj, int offset, int length) /*const*/ {
		return jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, offset, length);
	}
	
	class BoneChucker : jjBEHAVIORINTERFACE {
		void onBehave(jjOBJ@ obj) {
			obj.behave(BEHAVIOR::SKELETON);
			
			int playerID = obj.findNearestPlayer(150000);
			int fireRate = jjDifficulty >= 3? 12:(35 * (3 / (jjDifficulty + 1)));
			
			if (playerID > -1 && obj.freeze == 0) {
				if (facingPlayer(obj, playerID) && jjGameTicks % fireRate == 0) {
					jjOBJ@ bone = jjObjects[jjAddObject(OBJECT::SHARD, int(obj.xPos - (8 * obj.direction)), int(obj.yPos + 2), obj.objectID, CREATOR::OBJECT)];
					bone.behavior = BEHAVIOR::BOUNCERBULLET;
					bone.determineCurAnim(ANIM::SKELETON, 0);
					bone.playerHandling = HANDLING::ENEMYBULLET;
					bone.animSpeed = 1;
					bone.direction = obj.direction;
					bone.xSpeed = 6 * bone.direction;
					bone.ySpeed = -3;
					bone.state = STATE::FLY;
					bone.lightType = LIGHT::POINT;
					bone.counterEnd = 105;
					if (bone.counter == 0) randomBoneSample(bone);
					bone.killAnim = jjObjectPresets[OBJECT::BOUNCERBULLET].killAnim;
				}
			}
		}
	}
	
	void randomBoneSample(jjOBJ@ obj) {
		switch(jjRandom()%4) {
			case 0: jjSample(obj.xPos, obj.yPos, SOUND::SKELETON_BONE1, 0, 0); break;
			case 1: jjSample(obj.xPos, obj.yPos, SOUND::SKELETON_BONE2, 0, 0); break;
			case 2: jjSample(obj.xPos, obj.yPos, SOUND::SKELETON_BONE3, 0, 0); break;
			case 3: jjSample(obj.xPos, obj.yPos, SOUND::SKELETON_BONE5, 0, 0); break;
		}
	}
	
	bool facingPlayer(jjOBJ@ obj, int playerID) {
		if (((obj.xPos < (jjPlayers[playerID].xPos - 32) && obj.direction == 1) || (obj.xPos > (jjPlayers[playerID].xPos + 32) && obj.direction == -1)) && obj.yPos <= int(jjPlayers[playerID].yPos + 208) && obj.yPos >= int(jjPlayers[playerID].yPos - 208)) return true;
		return false;
	}
	
	class Fencer : Enemy {
		Fencer(jjOBJ@ preset) {
			super(preset);
			gender = Gender::Male;
			for (uint animID = 0; animID < 2; ++animID) {
				Recolor(jjAnimations[jjAnimSets[ANIM::FENCER] + animID], array<uint8>={24,48}, array<uint8>={38,39,79,79,79,79,79,79}); //red or pink to black
			}
		}		
		void onBehave(jjOBJ@ obj) override {
			if (obj.state == STATE::START) {
				obj.doesHurt = getParameterAtOrigin(obj, 0, 2);
				switch (obj.doesHurt) {
					case 0: //floor
						obj.behave(BEHAVIOR::FENCER, false);
						break;
					case 1: {
						int newYPos = int(obj.yPos) - 22;
						while (newYPos > 0 && !jjMaskedPixel(int(obj.xPos), newYPos))
							newYPos -= 2;
						obj.yPos = newYPos + 22;
						obj.state = STATE::DELAYEDSTART;
						break;
					}
					case 2:
					case 3: {
						int newXPos = int(obj.xPos);
						obj.direction = ((int(obj.doesHurt) - 2) << 1) - 1;
						while (!jjMaskedPixel(newXPos, int(obj.yPos)))
							newXPos += (obj.direction << 1);
						obj.xPos = newXPos - (obj.direction << 4);
						obj.state = STATE::STILL;
					}
				}
			} else if (obj.doesHurt == 1) { //hiding at ceiling
				if (Player.yPos > obj.yPos && abs(Player.xPos - obj.xPos) < 64) {
					obj.state = STATE::JUMP;
					obj.doesHurt = 0;
					obj.curAnim -= 1;
					obj.counter = obj.frameID = 0;
					jjSample(obj.xPos, obj.yPos, SOUND::COMMON_UP, 52);
					obj.direction = Player.direction;
					obj.xSpeed = -1 * obj.direction;
					obj.ySpeed = 1;
				}
			} else {
				if (obj.state == STATE::JUMP && obj.doesHurt != 0) {
					obj.xSpeed = (obj.doesHurt == 2) ? 4 : -4;
					obj.doesHurt = 0;
				}
				obj.behave(BEHAVIOR::FENCER, false);
				if (IsMurderer)// || (jjGameTicks & 1) == 1)
					obj.behave(BEHAVIOR::FENCER, false);
			}
		}
		void onDraw(jjOBJ@ obj) override {
			if (obj.state == STATE::DELAYEDSTART) //hiding in the ceiling
				return;
			else if (obj.doesHurt != 0)
				drawMainSprite(obj, -1, obj.doesHurt == 2 ? 0x300 : 0x100);
			else
				Enemy::onDraw(obj);
		}
	}
	
	
	//3 Tube Turtle
	class TubeTurle : Enemy {
		TubeTurle(jjOBJ@ preset) {
			super(preset, AnimSets::TubeTurle);
			preset.energy = 5;
		}
		void onBehave(jjOBJ@ obj) override {
			++obj.counter;
			if (obj.state == STATE::WAIT) {
				obj.curFrame = jjAnimations[obj.curAnim] + ((jjGameTicks >> 3) & 3);
				const float distanceFromPlayer = abs(Player.xPos - obj.xPos);
				if (distanceFromPlayer < 290)
					obj.direction = (obj.xPos > Player.xPos) ? -1 : 1;
				if ((distanceFromPlayer < 180 || IsMurderer) && obj.counter > 70) {
					obj.counter = 0;
					obj.state = STATE::ATTACK;
				}
			} else if (obj.state == STATE::ATTACK) {
				int frameID = obj.counter / 6;
				if (frameID > 12) {
					obj.state = STATE::WAIT;
					obj.counter = 0;
				} else if (frameID > 8)
					frameID = 12 - frameID;
				else if (frameID > 3) {
					frameID = 3;
					if (obj.counter % 6 == 5) {
						jjObjects[jjAddObject(OBJECT::BULLET, obj.xPos + 20*obj.direction, obj.yPos + 12, obj.objectID, CREATOR::OBJECT, TurleTube)].curAnim = jjAnimSets[animSetID];
						jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::TURTLE_SPK1TURT + (jjRandom() & 3)));
					}
				}
				obj.curFrame = jjAnimations[obj.curAnim + 1] + frameID;
			} else {
				obj.behave(BEHAVIOR::TUBETURTLE, false);
				return;
			}
			const jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
			if (!jjMaskedPixel(int(obj.xPos), int(obj.yPos) + frame.hotSpotY - frame.coldSpotY))
				obj.yPos += 1;
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
			if (bullet !is null && obj.state == STATE::FLOAT) {
				bullet.state = STATE::EXPLODE;
				obj.state = STATE::WAIT;
				obj.energy = 4;
				obj.curAnim = jjAnimSets[animSetID] + 1;
				obj.counter = 45;
				obj.yPos -= 20;
				jjSample(obj.xPos, obj.yPos, SOUND::TURTLE_TURN);
				return true;
			}
			return Enemy::onObjectHit(obj, bullet, player, force);
		}
	}
	void TurleTube(jjOBJ@ obj) {
		if (obj.state == STATE::START) {
			obj.xSpeed = jjObjects[obj.creatorID].direction * 5;
			obj.ySpeed = int(jjRandom() & 7) * -0.5;
			obj.state = STATE::FLY;
			obj.playerHandling = HANDLING::ENEMYBULLET;
			obj.animSpeed = 1;
			obj.killAnim = jjAnimSets[ANIM::AMMO] + 7;
		} else if (obj.state == STATE::EXPLODE) {
			obj.curAnim = obj.killAnim;
			obj.frameID = 0;
			obj.behavior = BEHAVIOR::EXPLOSION;
			obj.playerHandling = HANDLING::EXPLOSION;
		} else {
			if (jjMaskedPixel(int(obj.xPos), int(obj.yPos)))
				obj.state = STATE::EXPLODE;
			else {
				obj.xPos += obj.xSpeed;
				obj.yPos += obj.ySpeed += 0.1;
				obj.frameID = jjGameTicks / 3;
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.determineCurFrame());
			}
		}
	}
	
	//4 Float Lizard
	class FloatLizard : Enemy {
		Egg egg;
		FloatLizard(jjOBJ@ preset) {
			super(preset, AnimSets::Egg);
			egg.firstAnim = jjAnimSets[animSetID];
			preset.behavior = jjVOIDFUNCOBJ(behaviorAssigner);
			preset.energy = 1;
			gender = Male;
			Layer = 2;
			for (uint animID = 0; animID < 5; ++animID) {
				Recolor(jjAnimations[jjAnimSets[ANIM::LIZARD] + animID], array<uint8>={40,56}, array<uint8>={38,39,39,39,39,79,79,79}); //orange or yellow to black
			}
		}
		void behaviorAssigner(jjOBJ@ obj) {
			if (obj.creator != CREATOR::LEVEL || getParameterAtOrigin(obj, 1, 1) == 0) obj.behavior = this; else obj.behavior = egg;
		}
		
		void onBehave(jjOBJ@ obj) override {
			//if (obj.state == STATE::START) {
			//	obj.state = getParameterAtOrigin(obj, 0, 1) == 0 ? STATE::SLEEP : STATE::FALL;
			//}
			
			//const int xDiff = 96 * obj.direction;
			//Player.yPos -= 96; //target ABOVE me
			//Player.xPos += xDiff;
			//obj.behave(BEHAVIOR::FLOATLIZARD, true);
			//Player.yPos += 96;
			//Player.xPos -= xDiff;
			obj.behave(BEHAVIOR::FLOATLIZARD, true);
			obj.state = STATE::WALK;
			if ( 0<=abs((int(Player.xPos)>>>5) - (int(obj.xPos)>>>5)) && abs((int(Player.xPos)>>>5) - (int(obj.xPos)>>>5)) <=5 && 0<=abs((int(Player.yPos)>>>5) - (int(obj.yPos)>>>5)) && abs((int(Player.yPos)>>>5) - (int(obj.yPos)>>>5)) <=5) {
				//jjDebug((Player.xPos>>5)+"  "+(Player.yPos>>5)+"  "+(obj.xPos>>5)+"  "+(obj.yPos>>5)+"  "+abs((Player.xPos>>5) - (obj.xPos>>5))+"  "+abs((Player.yPos>>5) - (obj.yPos>>5)));
				jjObjects[jjAddObject(OBJECT::FLOATLIZARD, obj.xPos, obj.yPos, obj.creatorID, CREATOR::LEVEL, BEHAVIOR::FLOATLIZARD)];
				obj.delete();
			}
			
			
			/*if (obj.state == STATE::WALK) {
				obj.var[1] = 0; //counter to entering STATE::ATTACK
				if (obj.yPos < Player.yPos && obj.yPos > Player.yPos - 192 && abs(obj.xPos - Player.xPos) < 24 && !jjMaskedVLine(int(obj.xPos), int(obj.yPos) - 5, 30)) {
					//obj.state = STATE::FLY;
					//obj.behavior = BEHAVIOR::FLOATLIZARD;
					jjObjects[jjAddObject(OBJECT::FLOATLIZARD, obj.xPos, obj.yPos, obj.creatorID, CREATOR::LEVEL, BEHAVIOR::FLOATLIZARD)];
					//obj.counter = 0;
					//obj.frameID = 0;
					//obj.direction = (obj.xPos > Player.xPos) ? -6 : 6;
					obj.delete();
				}
			} else if ((obj.state == STATE::WALK || obj.state == STATE::WAIT) && ++obj.counter > 70*4) {
				obj.state = STATE::SLEEP; //return to the air
				obj.ySpeed = -8;
				obj.xSpeed *= 2;
			} else if (obj.state == STATE::SLEEP) { //just got off the ground
				obj.xPos += (obj.xSpeed *= 0.9) + jjSin(jjGameTicks << 3) / 2;
				obj.yPos += (obj.ySpeed *= 0.9) + jjCos(jjGameTicks << 3);
			}*/
		}
		/*void onDraw(jjOBJ@ obj) override {
			if (obj.state == STATE::FLY || obj.state == STATE::SLEEP || (obj.state == STATE::DONE && (obj.oldState == STATE::FLY || obj.oldState == STATE::SLEEP)))
				jjDrawSprite(obj.xPos, obj.yPos + 4, ANIM::LIZARD, 3, jjGameTicks/3, obj.direction, SPRITE::NORMAL,0, Layer);
			Enemy::onDraw(obj);
		}*/
	}
	class Egg : jjBEHAVIORINTERFACE {
		uint firstAnim;
		void onBehave(jjOBJ@ obj) {
			if (obj.state == STATE::START) {
				obj.curFrame = jjAnimations[firstAnim];
				obj.xPos += int(jjRandom() & 31) - 15;
				obj.putOnGround();
				obj.yPos += jjRandom() & 3;
				obj.isFreezable = false; //let's just not bother
				obj.isTarget = false;
				if (jjParameterGet(uint(obj.xOrg)>>5, uint(obj.yOrg)>>5, 0, 1) == 0) { //real egg
					obj.playerHandling = HANDLING::SPECIAL;
					obj.scriptedCollisions = true;
					obj.bulletHandling = HANDLING::DETECTBULLET;
					obj.counterEnd = 4;
					obj.energy = 30;
				} else { //false egg
					obj.playerHandling = HANDLING::PARTICLE;
					obj.counterEnd = (jjRandom() & 2) + 3;
				}
				obj.state = STATE::WAIT;
			} else if (obj.state == STATE::DEACTIVATE) {
				obj.deactivate();
			} else if (obj.counterEnd == 4) { //real egg, needing to hatch
				if (obj.counter > 0) {
					if (++obj.counter >= 210)
						hatch(obj);
					else
						obj.curFrame = jjAnimations[firstAnim] + obj.counter / 70;
				}
			}
		}
		void onDraw(jjOBJ@ obj) {
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 1, obj.counterEnd != 4 ? SPRITE::TINTED : SPRITE::NORMAL, 79, obj.counterEnd);
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
			if (bullet !is null) {
				obj.counter += force*70;
				bullet.state = STATE::EXPLODE;
			} else {
				if (obj.counter == 0) obj.counter = 1;
			}
			return true;
		}
		void hatch(jjOBJ@ obj) const {
			jjSample(obj.xPos, obj.yPos, SOUND::COMMON_CANSPS);
			jjParameterSet(uint(obj.xOrg)>>5, uint(obj.yOrg)>>5, 1, 1, 0); //only raptors now
			jjOBJ@ blacky = jjObjects[jjAddObject(obj.eventID, obj.xOrg, obj.yOrg, 0, CREATOR::LEVEL)];
			blacky.xPos = obj.xPos;
			blacky.yPos = obj.yPos - 24;
			for (uint i = 0; i < 6; ++i) {
				jjOBJ@ shard = jjObjects[jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos - 24, obj.creatorID, CREATOR::OBJECT, BEHAVIOR::INACTIVE)];
				shard.curAnim = firstAnim + 1 + i;
				shard.behavior = BEHAVIOR::SHARD;
			}
			obj.delete();
		}
	}
	
	class Fish : Enemy {
		Fish(jjOBJ@ preset) {
			super(preset);
			preset.energy = 1;
			preset.isFreezable = false;
			preset.isTarget = false;
			for (uint animID = 0; animID < 2; ++animID) {
				Recolor(jjAnimations[jjAnimSets[ANIM::FISH] + animID], array<uint8>={32}, RecolorReplace(80));
				Recolor(jjAnimations[jjAnimSets[ANIM::FISH] + animID], array<uint8>={48}, RecolorReplace(64));
			}
		}
		void onBehave(jjOBJ@ obj) override {
			if (obj.state == STATE::START)
				obj.state = STATE::DELAYEDSTART;
			else if (obj.state == STATE::DEACTIVATE)
				obj.deactivate();
			else if (obj.state == STATE::FREEZE)
				obj.behave(BEHAVIOR::FISH, false); //as good as any other for unfreezing
			else {
				if (obj.state == STATE::DELAYEDSTART) {
					if (obj.yPos > jjWaterLevel) {
						obj.state = STATE::FLY;
						obj.isFreezable = true;
						obj.isTarget = true;
					}
				} else if (obj.state != STATE::DONE && obj.state != STATE::KILL) {
					if (int(obj.xSpeed) == 0 && int(obj.ySpeed) == 0) {
						const int divide = 1;//IsMurderer ? 1 : 2;
						const float targetX = obj.xOrg + ((jjRandom() & 255) - 127.5) / divide;
						const float targetY = obj.yOrg + ((jjRandom() & 255) - 127.5) / divide;
						obj.xSpeed = (targetX - obj.xPos) / 16 / divide;
						obj.ySpeed = (targetY - obj.yPos) / 16 / divide;
						obj.direction = obj.xSpeed >= 0 ? 1 : -1;
					} else {
						if (jjMaskedPixel(int(obj.xPos + obj.xSpeed), int(obj.yPos)))
							obj.xSpeed = -obj.xSpeed;
						else
							obj.xPos += obj.xSpeed;
						if (jjMaskedPixel(int(obj.xPos), int(obj.yPos + obj.ySpeed)))
							obj.ySpeed = -obj.ySpeed;
						else
							obj.yPos += obj.ySpeed;
						obj.xSpeed *= 0.94;
						obj.ySpeed *= 0.94;
					}
					if (obj.yPos < jjWaterLevel)
						obj.yPos = jjWaterLevel;
				}
				obj.frameID = jjGameTicks >> 3;
				obj.determineCurFrame();
			}
		}
		void onDraw(jjOBJ@ obj) override {
			if (obj.state == STATE::DELAYEDSTART) //yet to be activated by the water
				return;
			else
				Enemy::onDraw(obj);
		}
	}
	
	class YetiCrab : jjBEHAVIORINTERFACE {
		void onBehave(jjOBJ@ obj) {
			obj.behave(BEHAVIOR::CRAB);
			
			if (obj.xSpeed > 0 && obj.direction == 1) obj.xPos += 1;
			else if (obj.xSpeed < 0 && obj.direction == -1) obj.xPos -= 1;
			
			if (jjGameTicks % 70 == 1) {
				IceBullet temp;
				jjOBJ@ icebullet = jjObjects[jjAddObject(OBJECT::LIGHTNINGSHIELDBULLET, int(obj.xPos), int(obj.yPos), obj.objectID, CREATOR::OBJECT)];//, jjVOIDFUNCOBJ(temp.onBehave))];
				icebullet.behavior = BEHAVIOR::PEPPERBULLET;
				jjSample(icebullet.xPos, icebullet.yPos, SOUND::AMMO_ICEPU3);
				icebullet.xPos = obj.xOrg + (8 * obj.direction);
				icebullet.counterEnd = 30;
				icebullet.state = STATE::FLY;
				icebullet.playerHandling = HANDLING::ENEMYBULLET;
			}
		}
	}
	
	class DragonFly : jjBEHAVIORINTERFACE {
		void onBehave(jjOBJ@ obj) {
			obj.behave(BEHAVIOR::DRAGONFLY);
			
			if (jjGameTicks % 70 == 1) {
				IceBullet temp;
				jjOBJ@ icebullet = jjObjects[jjAddObject(OBJECT::RFBULLETPU, int(obj.xPos), int(obj.yPos), obj.objectID, CREATOR::OBJECT)];//, jjVOIDFUNCOBJ(temp.onBehave))];
				icebullet.behavior = BEHAVIOR::RFBULLET;
				//jjSample(icebullet.xPos, icebullet.yPos, SOUND::AMMO_ICEPU3);
				//icebullet.xPos = obj.xOrg + (8 * obj.direction);
				//icebullet.counterEnd = 30;
				//icebullet.state = STATE::FLY;
				icebullet.playerHandling = HANDLING::ENEMYBULLET;
			}
		}
	}
	
	//5 Red Bat
	class RedBat : Enemy {
		RedBat(jjOBJ@ preset) {
			super(preset);
			preset.energy = 2;
			preset.curAnim -= 1;
			gender = Gender::Female;
			//for (uint animID = 0; animID < 1; ++animID) { //lul
			//	Recolor(jjAnimations[jjAnimSets[ANIM::BAT] + animID], array<uint8>={88}, RecolorReplace(24));
			//}
		}
		void onBehave(jjOBJ@ obj) override {
			if (obj.state != STATE::FLY) obj.behave(BEHAVIOR::BAT);
			
			if (obj.state == STATE::START){
//				obj.state = STATE::FLY; //don't stick to the ceiling like regular bats
//				obj.behave(BEHAVIOR::BAT, true);
			}else if (obj.state == STATE::DEACTIVATE || obj.state == STATE::FREEZE){
				obj.behave(BEHAVIOR::BAT, false); //as good as any other for unfreezing
			}else if (obj.state == STATE::FLY) { //I feel bad about not making any call to jjOBJ::behave in here but there's not a lot of bat code to begin with
				//obj.behave(BEHAVIOR::BAT, false);
				obj.doesHurt = jjSampleLooped(obj.xPos,obj.yPos,SOUND::BAT_BATFLY1,obj.doesHurt,43,0);

				const float wantx = Player.xPos;
				const float wanty = Player.yPos - 96;
				
				//cut out some code here about making the bat return to its place of origin if the player is too far away

				const float xDiff = int(jjRandom() & 32767) / 10923.6667f;
				const float xTarget = obj.xPos + (wantx<obj.xPos ? -xDiff : xDiff);
				if (!jjMaskedPixel(int(xTarget), int(obj.yPos)))
					obj.xPos = xTarget;
				const float yTarget = obj.yPos + obj.ySpeed;
				/*if (yTarget <= jjWaterLevel && !jjMaskedPixel(int(obj.xPos), int(yTarget)))
					obj.yPos = yTarget;
				*/
				if (++obj.counter > 7) {
					if (wantx<obj.xPos-1)
						obj.direction=-1;
					else
					if (wantx>obj.xPos+1)
						obj.direction=1;
						
					//if (player.yPos > obj.yPos && obj.yPos < (obj.yOrg + 96)) obj.yPos += 1;
					if (wanty < obj.yPos)
						obj.yPos -= 5;
					else
						obj.yPos += 5;
					/*if (wanty < obj.yPos)
						obj.ySpeed = int(jjRandom() & 32767) / -16384.f;
					else
						obj.ySpeed = int(jjRandom() & 32767) / 16384.f;
					*/
					if (++obj.counterEnd > 14) {
						obj.counterEnd = 0;
						jjOBJ@ bullet = jjObjects[jjAddObject(OBJECT::ELECTROBULLET, obj.xPos, obj.yPos, 0, CREATOR::OBJECT)];
						bullet.playerHandling = HANDLING::ENEMYBULLET;
						bullet.xSpeed = (wantx - obj.xPos) / 32.f;//(IsMurderer ? 32.f : 128.f);
						bullet.ySpeed = 3;
						jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::AMMO_LASER2 + (jjRandom()&1)));
					}

					obj.counter = 0;
					++obj.frameID;
					obj.determineCurFrame();
				}
			}
		}
	}
	
	//6 Monkeys
	bool LevelContainsMonkeys, LevelContainsLava;
	class Monkey : Enemy {
		Monkey(jjOBJ@ preset) {
			super(preset);
			gender = Gender::Male;
			if (!LevelContainsMonkeys) {
				LevelContainsMonkeys = true;
				for (uint animID = 0; animID < 7; ++animID) {
					const jjANIMATION@ anim = jjAnimations[jjAnimSets[ANIM::MONKEY] + animID];
					if (animID >= 2) {
						Recolor(anim, array<uint8>={40}, RecolorReplace(24)); //orange to red
						Recolor(anim, array<uint8>={64}, RecolorReplace(36)); //brown to orange
					} else {
						Resize::Resize(anim, 1.75, Resize::Method::Scale2x);
					}
				}
			}
		}
		void onBehave(jjOBJ@ obj) override {
			for (uint i = (IsMurderer && jjGameTicks&1==1) ? 2 : 1; i > 0; --i)
				obj.behave(BEHAVIOR::MONKEY, false); //both MONKEY and STANDMONKEY use the same behavior... not very efficient, arjan
		}
	}
	void DoObjectLoop() {
		if (LevelContainsMonkeys && (jjDifficulty > 0 || IsMurderer)) {
			for (uint i = jjObjectCount; --i > 0;) {
				jjOBJ@ obj = jjObjects[i];
				if (obj.behavior == BEHAVIOR::MONKEYBULLET) {
					if (obj.state == STATE::EXPLODE) {
						obj.behavior = BEHAVIOR::RFBULLET;
						obj.playerHandling = HANDLING::ENEMYBULLET;
						obj.eventID = OBJECT::RFBULLETPU;
						obj.curAnim = obj.killAnim;
						obj.frameID = 0;
						obj.blast(96*96, false);
					} else {
						obj.lightType = LIGHT::BRIGHT;
						obj.light = 6;
					}
				}
			}
		}
	}
	
	class IceBullet : jjBEHAVIORINTERFACE {
		void onBehave(jjOBJ@ obj) {
			obj.behave(BEHAVIOR::BULLET, true);
			obj.direction = obj.xSpeed < 1? -1:1;
			int playerID = obj.findNearestPlayer(30000);
			if (obj.state == STATE::FLY && obj.doesCollide(jjPlayers[playerID], true)) {
				jjPlayers[playerID].freeze(true);
				jjPlayers[playerID].xSpeed -= (obj.xSpeed / (obj.direction == -1? 8:-8)) * obj.direction;
			}
		}
	}
	void handleEnemyProjectiles() {
		for (int i = 0; i < jjObjectCount; i++) {
			jjOBJ@ obj = jjObjects[i];
			if (obj.behavior == BEHAVIOR::TOASTERBULLET && obj.creatorType != CREATOR::PLAYER && jjGameTicks % 1 == 0 ) {
				IceBullet temp;
				jjOBJ@ icebullet = jjObjects[jjAddObject(OBJECT::ICEBULLETPU, int(obj.xPos), int(obj.yPos), obj.objectID, CREATOR::OBJECT, jjVOIDFUNCOBJ(temp.onBehave))];
				jjSample(icebullet.xPos, icebullet.yPos, SOUND::AMMO_ICEPU3);
				icebullet.xPos = obj.xOrg + (8 * obj.direction);
				icebullet.counterEnd = 30;
				icebullet.state = STATE::FLY;
				icebullet.playerHandling = HANDLING::ENEMYBULLET;
				obj.delete();
			}
			/*
			if (obj.behavior == BEHAVIOR::MONKEYBULLET) {
				Snowball temp;
				jjOBJ@ snowball = jjObjects[jjAddObject(OBJECT::BOUNCERBULLET, int(obj.xPos), int(obj.yPos), obj.objectID, CREATOR::OBJECT, jjVOIDFUNCOBJ(temp.onBehave))];
				snowball.playerHandling = HANDLING::ENEMYBULLET;
				jjSample(snowball.xOrg, snowball.yOrg, SOUND::COMMON_FOEW2, 0, 0);
				snowball.xAcc = snowball.yAcc = 0;
				snowball.xSpeed = 8 * obj.direction;
				snowball.yPos = snowball.yOrg;
				int playerID = snowball.findNearestPlayer(80000);
				if (jjPlayers[playerID].yPos > int(snowball.yPos)) snowball.ySpeed = (jjPlayers[playerID].yPos > int(snowball.yPos) + 64)? 2:0;
				else if (jjPlayers[playerID].yPos < int(snowball.yPos)) snowball.ySpeed = (jjPlayers[playerID].yPos < int(snowball.yPos) - 64)? -4:-2;
				snowball.direction = obj.direction;
				snowball.counterEnd = 140;
				snowball.animSpeed = 1;
				snowball.light = 8;
				snowball.state = STATE::FLY;
				snowball.killAnim = jjObjectPresets[OBJECT::ICEBULLET].killAnim;
				obj.delete();
			}*/
		}
	}
	
	//7 Muttshroom
	class Muttshroom : Enemy {
		uint firstMushroomFrame;
		Muttshroom(jjOBJ@ preset) {
			super(preset, AnimSets::Muttshroom);
			firstMushroomFrame = jjAnimations[jjAnimSets[animSetID]];
			for (uint animID = 0; animID < 2; ++animID) {
				const jjANIMATION@ anim = jjAnimations[jjAnimSets[ANIM::DOG] + animID];
				Recolor(anim, array<uint8>={64}, RecolorReplace(24)); //brown to red
				Recolor(anim, array<uint8>={40}, RecolorReplace(62)); //orange to brown
			}
			if (true) {
				for (uint i = 0; i < 5; ++i) { //copter
					jjANIMFRAME@ frame = jjAnimFrames[firstMushroomFrame + 5 + i];
					jjPIXELMAP image(frame);
					for (int x = image.width - 1; x >= 0; --x)
						for (int y = image.height - 1; y >= 0; --y)
							if (image[x,y] != 0)
								image[x,y] += 16;
					image.save(frame);
				}
			}
		}
		void onBehave(jjOBJ@ obj) override {
			switch (obj.state) {
				case STATE::ROTATE:
					obj.counter += 4;
					obj.xPos = obj.xOrg + jjSin(obj.counter) * 48;
					obj.yPos = obj.yOrg + jjCos(obj.counter) * 48;
					obj.direction = (obj.counter - 256) & 1023 < 512 ? 1 : -1;
					if (Player.blink == 0 && jjAnimFrames[Player.curFrame].doesCollide(int(Player.xPos), int(Player.yPos), Player.direction, jjAnimFrames[firstMushroomFrame+5], int(obj.xPos), int(obj.yPos), 1)) //player collides with copter
						Player.hurt(2);
					if (obj.counterEnd == 0)
						obj.counterEnd = jjRandom() & 63;
					else if ((++obj.counterEnd > 200 || IsMurderer) && jjGameTicks & 1 == 1)
						jjAddObject(obj.eventID, obj.xPos, obj.yPos+20, obj.creatorID); //spores
					break;
				case STATE::FLY: //copter part
					if ((obj.counterEnd -= 3) < 30)
						obj.delete();
					else {
						obj.xPos += obj.xSpeed *= 0.97;
						obj.yPos += obj.ySpeed *= 0.97;
					}
					break;
				case STATE::START:
					if (obj.creatorType == CREATOR::LEVEL) {
						if (getParameterAtOrigin(obj, 0, 1) == 1) {
							obj.state = STATE::ROTATE;
							obj.curFrame = firstMushroomFrame + 3;
							obj.energy = 3;
							break;
						}
						obj.energy = 2;
					} else {
						obj.state = STATE::FLY;
						obj.curFrame = firstMushroomFrame + 4;
						obj.counterEnd = 255;
						const auto angle = jjRandom();
						obj.xSpeed = jjSin(angle) * 4;
						obj.ySpeed = jjCos(angle) * 4;
						obj.isTarget = false;
						obj.isFreezable = false;
						break;
					}
				default:
					obj.behave(BEHAVIOR::DOGGYDOGG, false);
					if (obj.state == STATE::WALK && (jjRandom() & 127) == 0) {
						jjSample(obj.xPos, obj.yPos, SOUND::DOG_AGRESSIV);
						obj.var[0] = -1;
						obj.var[1] = 255;
						obj.counter=jjGameTicks&7;	//aligning anim
						obj.state = STATE::ACTION;
					}
			}
		}
		void onDraw(jjOBJ@ obj) override {
			if (obj.state == STATE::FLY) {
				const float scale = obj.counterEnd / 255.f;
				jjDrawResizedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, scale,scale, SPRITE::ALPHAMAP, 42);
			} else {
				if (obj.curFrame > firstMushroomFrame) {
					const auto realDirection = obj.direction;
					obj.direction = 1;
					drawMainSprite(obj, firstMushroomFrame + 5 + ((obj.counter >> 3) % 5));
					obj.direction = realDirection;
				}
				Enemy::onDraw(obj);
				if (obj.curFrame < firstMushroomFrame) {
					const uint frameID = (jjGameTicks >> 2) & 3;
					const uint mushroomFrame = firstMushroomFrame + (obj.state == STATE::DONE ? 0 : (frameID < 3 ? frameID : 1));
					drawBodySprite(obj, obj.xPos + obj.direction * 13, obj.yPos + (obj.curAnim == jjObjectPresets[obj.eventID].curAnim ? 2 : -5), mushroomFrame);
					drawBodySprite(obj, obj.xPos - obj.direction * 18, obj.yPos - 6, mushroomFrame, 128 * obj.direction, 0.5f);
				}
			}
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
			if (obj.state != STATE::FLY || (bullet is null && obj.counterEnd > 50)) {
				if (jjColorDepth >= 16 && player.shieldTime <= 0) {
					if (Player.stoned <= 0)
						jjSample(Player.xPos, Player.yPos, SOUND::CATERPIL_RIDOE);
					if (Player.stoned < 15)
						Player.stoned = IsMurderer ? 140 : 55;
				}
			}
			if (obj.state != STATE::FLY) {
				const bool isFlier = obj.state == STATE::ROTATE;
				Enemy::onObjectHit(obj,bullet,player,force);
				if (!obj.isActive && isFlier) { //murdered
					jjObjects[jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos)].curAnim = jjAnimSets[animSetID] + 1; //lose copter as particle
				}
			} else if (bullet is null)
				obj.delete();
			return true;
		}
	}
	
	//8 Trogdor
	class Trogdor : Enemy {
		Trogdor(jjOBJ@ preset) {
			super(preset, AnimSets::Trogdor);
			preset.energy = 3;
			preset.curAnim = jjAnimSets[animSetID];
			preset.determineCurFrame();
			preset.xSpeed = 0.7;
			gender = Gender::Male;
		}
		void onBehave(jjOBJ@ obj) override {
			obj.behave(BEHAVIOR::WALKINGENEMY, false);
			if (obj.state != STATE::FREEZE && obj.state != STATE::DONE && (jjGameTicks & 3) == 3 && (IsMurderer || (obj.frameID % 7) == 3)) {
				jjSample(obj.xPos, obj.yPos, SOUND::AMMO_FIREGUN2A);
				jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::TOASTERBULLET)];
				bullet.xPos += 40 * obj.direction;
				bullet.yPos += 20;
				bullet.killAnim = bullet.determineCurAnim(ANIM::AMMO, 0, false);
				bullet.playerHandling = HANDLING::ENEMYBULLET;
				bullet.state = STATE::FLY;
				bullet.animSpeed = 1;
				bullet.xSpeed /= 4;
				bullet.xAcc /= 3;
				bullet.yAcc = (int(jjRandom() & 3) - 1) / 64.f; //replicates normal spreading behavior for player-fired toaster
			}
		}
	}
	
	//9 Frogs
	const uint FrogDistanceFromGround = 18;
	class Frog : Enemy {
		Frog(jjOBJ@ preset) {
			super(preset);
			preset.energy = 2;
			preset.determineCurAnim(ANIM::FROG, 2);
			preset.determineCurFrame();
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
			Enemy::onObjectHit(obj,bullet,player,force);
			if (obj.energy <= 0) {
				for (uint i = jjObjectCount; --i > 0;) {
					jjOBJ@ obj2 = jjObjects[i];
					if (obj2.eventID == OBJECT::DRAGONFLY && obj2.isActive && obj2.state != STATE::DONE && obj2.creatorID == uint(obj.objectID))
						obj2.state = STATE::ATTACK;
				}
			}
			return true;
		}
		void onBehave(jjOBJ@ obj) override {
			if (obj.state == STATE::START) {
				obj.state = STATE::IDLE;
				obj.yPos += jjMaskedTopVLine(int(obj.xPos), int(obj.yPos + FrogDistanceFromGround), 200);
				for (uint i = 0; i < 3; ++i)
					jjAddObject(OBJECT::DRAGONFLY, obj.xPos, obj.yPos, obj.objectID);
			} else if (obj.state == STATE::DEACTIVATE) {
				for (uint i = jjObjectCount; --i > 0;) {
					jjOBJ@ obj2 = jjObjects[i];
					if (obj2.eventID == OBJECT::DRAGONFLY && obj2.isActive && obj2.state != STATE::DONE && obj2.creatorID == uint(obj.objectID)) {
						obj2.delete();
					}
				}
				if (obj.var[0] == 0)
					obj.deactivate();
				else {
					obj.delete();
					removeFromEventMap(obj);
				}
			} else if (obj.state == STATE::FREEZE)
				obj.behave(BEHAVIOR::WALKINGENEMY, false);
			else if (obj.state != STATE::DONE) {
				const float delta = abs(obj.xPos - Player.xPos);
				if (delta > 16)
					obj.direction = (obj.xPos > Player.xPos) ? -1 : 1;
				if (obj.state != STATE::JUMP && obj.state != STATE::FALL && !jjMaskedHLine(int(obj.xPos) - 12, 24, int(obj.yPos) + FrogDistanceFromGround)) {
					obj.determineCurAnim(ANIM::FROG, 0);
					obj.state = STATE::FALL;
					obj.ySpeed = 0;
				} else if (obj.state == STATE::FIRE) {
					if (++obj.counter >= 14*5) {
						obj.determineCurAnim(ANIM::FROG, 2);
						obj.state = STATE::IDLE;
						obj.counter = 0;
					}
				} else if (obj.state == STATE::IDLE) {
					if (++obj.counter > 80) {
						if (delta > 32 && delta < 256 && abs(obj.yPos - Player.yPos) < 192) {
							if (Player.direction == obj.direction) { //looking away from me
								obj.determineCurAnim(ANIM::FROG, jjIsTSF ? 12 : 11);
								obj.state = STATE::WALK;
								obj.xSpeed = 1.5;
							} else { //looking at me
								obj.determineCurAnim(ANIM::FROG, 5);
								obj.state = STATE::JUMP;
								obj.ySpeed = -3.75;
								obj.xSpeed = -2.3;
								jjSample(obj.xPos, obj.yPos, SOUND::COMMON_JUMP);
							}
						} else if (obj.counter >= 100) {
							obj.counter = 0;
							obj.state = STATE::FIRE;
							obj.determineCurAnim(ANIM::FROG, (jjIsTSF ? 8 : 7) + (jjRandom() % 3));
							jjSample(obj.xPos, obj.yPos, SOUND::FROG_TONG);
						}
					}
				} else {
					if (!jjMaskedVLine(int(obj.xPos) + (obj.xSpeed >= 0 ? 19 : -19) * obj.direction, int(obj.yPos) - 10, 4 + FrogDistanceFromGround))
						obj.xPos += obj.xSpeed * obj.direction;
					else if (obj.state == STATE::WALK) {
						obj.determineCurAnim(ANIM::FROG, 2);
						obj.state = STATE::IDLE;
						obj.counter = 0;
					}
					if (obj.state != STATE::WALK && obj.state != STATE::IDLE) {
						if ((obj.ySpeed += 0.25) >= 0 && obj.state == STATE::JUMP) {
							obj.determineCurAnim(ANIM::FROG, 0);
							obj.state = STATE::FALL;
						} else if (jjMaskedHLine(int(obj.xPos) - 11, 22, int(obj.yPos += obj.ySpeed) + FrogDistanceFromGround)) {
							if (obj.xSpeed != 1.5) {
								if (obj.state == STATE::FALL) {
									obj.determineCurAnim(ANIM::FROG, 2);
									obj.counter = 30;
									obj.yPos -= FrogDistanceFromGround * 2;
									obj.yPos += jjMaskedTopVLine(int(obj.xPos), int(obj.yPos + FrogDistanceFromGround), 200);
									obj.state = STATE::IDLE;
								}
							} else {
								obj.determineCurAnim(ANIM::FROG, jjIsTSF ? 12 : 11);
								obj.state = STATE::WALK;
							}
						}
					} else if (delta <= 16) {
						obj.determineCurAnim(ANIM::FROG, 2);
						obj.state = STATE::IDLE;
						obj.counter = 0;
						obj.xSpeed = 0;
					}
				}
				obj.frameID = (obj.state != STATE::FIRE ? jjGameTicks : obj.counter) / 5 ;
				obj.determineCurFrame();
			}
		}
	}
	class FrogFly : Enemy {
		FrogFly(jjOBJ@ preset) {
			super(preset);
			preset.energy = 1;
			preset.deactivates = false;
			preset.determineCurAnim(ANIM::DRAGFLY, 0);
		}
		void onBehave(jjOBJ@ obj) override {
			switch (obj.state) {
				case STATE::START:
					if (obj.creatorType == CREATOR::OBJECT) {
						obj.state = STATE::CIRCLE;
						obj.counter = jjRandom();
					} else {
						jjDebug("something went wrong with a frogfly :(");
						obj.delete();
					}
					break;
				case STATE::DEACTIVATE:
				case STATE::FREEZE:
					obj.behave(BEHAVIOR::DRAGONFLY, false);
					return;
				case STATE::CIRCLE: {
					obj.counter += 5 + (jjRandom() & 7);
					const float targetX = jjObjects[obj.creatorID].xPos + jjSin(obj.counter) * 40, targetY = jjObjects[obj.creatorID].yPos - 45 + jjCos(obj.counter) * 15;
					if (obj.xPos > targetX + 3) {
						obj.xPos -= 1;
						obj.direction = -1;
					} else if (obj.xPos < targetX - 3) {
						obj.xPos += 1;
						obj.direction = 1;
					}
					if (obj.yPos > targetY + 3) {
						obj.yPos -= 1;
					} else if (obj.yPos < targetY - 3) {
						obj.yPos += 1;
					}
					break; }
				case STATE::ATTACK:
					obj.counter += 14;
					if ((obj.counterEnd += 2) == 20) {
						obj.special = GetAngle(Player.yPos - obj.yPos, obj.xPos - Player.xPos);
						obj.deactivates = true;
						obj.direction = jjSin(obj.special) >= 0 ? 1 : -1;
					} else if (obj.counterEnd > 70) {
						obj.xPos += jjSin(obj.special) * 6;
						obj.yPos += jjCos(obj.special) * 6;
					}
					break;
				default:
					return;
			}
			obj.frameID = obj.counter >> 4;
			obj.determineCurFrame();
			//obj.doesHurt = jjSampleLooped(obj.xPos,obj.yPos,SOUND::DRAGFLY_BEELOOP,obj.doesHurt, obj.state == STATE::ATTACK ? 63 : 20);
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
			Enemy::onObjectHit(obj,bullet,player,force);
			if (obj.energy <= 0) {
				jjObjects[obj.creatorID].var[0] = 1; //has lost a fly; should be deleted instead of deactivated, to avoid letting the player kill more enemies than exist.
			}
			return true;
		}
	}
	
	//10 Bilsy
	class MiniBilsy : Enemy {
		MiniBilsy(jjOBJ@ preset) {
			super(preset);
			preset.energy = 4;
			preset.isFreezable = false;
			preset.isTarget = false;
			gender = Gender::Male;
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
			if (obj.state == STATE::DELAYEDSTART) {
				if (bullet is null) {
					obj.isFreezable = true;
					obj.isTarget = true;
					obj.state = STATE::FADEIN;
					obj.determineCurAnim(ANIM::BILSBOSS, 1);
					obj.determineCurFrame();
					obj.putOnGround(true);
					obj.direction = player.direction;
					obj.xPos -= obj.direction * 96;
					jjSample(obj.xPos, obj.yPos, SOUND::BILSBOSS_THUNDER);
				}
			} else
				Enemy::onObjectHit(obj,bullet,player,force);
			return true;
		}
		void onBehave(jjOBJ@ obj) override {
			switch (obj.state) {
			case STATE::START:
				obj.state = STATE::DELAYEDSTART;
				break;
			case STATE::FREEZE:
			case STATE::DEACTIVATE:
				obj.behave(BEHAVIOR::TUBETURTLE);
				break;
			case STATE::IDLE:
				obj.frameID = (jjGameTicks >> 2) & 7;
				obj.determineCurFrame();
				switch (jjRandom() & 127) {
					case 0:
						obj.state = STATE::ATTACK;
						obj.determineCurAnim(ANIM::BILSBOSS, 0);
						obj.counter = 0;
						break;
					case 1: //teleport
					case 2:
					case 3:
						obj.state = STATE::FADEOUT;
						obj.determineCurAnim(ANIM::BILSBOSS, 2);
						obj.counter = 0;
						if (IsMurderer) { //invincible while transportalizing
							obj.playerHandling = HANDLING::PARTICLE;
						} {
							obj.xAcc = Player.xPos;
							obj.yAcc = Player.yPos;
						}
						break;
				}
				break;
			case STATE::FADEOUT:
				if (++obj.counter < 68) {
					obj.frameID = obj.counter >> 2;
					obj.determineCurFrame();
				} else {
					obj.frameID = obj.counter = 0;
					obj.state = STATE::FADEIN;
					jjSample(obj.xPos, obj.yPos, SOUND::BILSBOSS_BILLAPPEAR);
					obj.determineCurAnim(ANIM::BILSBOSS, 1);
					obj.xPos = obj.xAcc;
					obj.yPos = obj.yAcc - 32;
					obj.direction = (Player.xPos < obj.xPos) ? -1 : 1;
					obj.determineCurFrame();
					obj.putOnGround(true);
					return;
				}
				break;
			case STATE::FADEIN:
				if (++obj.counter < 40) {
					obj.frameID = obj.counter >> 1;
					obj.determineCurFrame();
				} else {
					obj.frameID = obj.counter = 0;
					obj.determineCurAnim(ANIM::BILSBOSS, 4);
					obj.state = STATE::IDLE;
					obj.playerHandling = HANDLING::SPECIAL;
				}
				break;
			case STATE::ATTACK:
				if (++obj.counter < 36) {
					obj.frameID = obj.counter >> 1;
					obj.determineCurFrame();
					if (obj.counter == 32) {
						jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::FIREBALLBULLET)];
						bullet.direction = obj.direction;
						bullet.xAcc = abs(bullet.xAcc) * bullet.direction;
						bullet.xSpeed = abs(bullet.xSpeed) * bullet.direction / 3;
						bullet.state = STATE::FLY;
						bullet.playerHandling = HANDLING::ENEMYBULLET;
						bullet.animSpeed = IsMurderer ? 2 : 1;
						bullet.determineCurAnim(ANIM::BILSBOSS, 3);
					}
				} else {
					obj.frameID = obj.counter = 0;
					obj.determineCurAnim(ANIM::BILSBOSS, 4);
					obj.state = STATE::IDLE;
				}
				break;
			}
		}
		void onDraw(jjOBJ@ obj) override {
			if (obj.state != STATE::DELAYEDSTART)
				Enemy::onDraw(obj);
			//else //just for testing
			//	jjDrawSpriteFromCurFrame(obj.xPos,obj.yPos,obj.curFrame,obj.direction,SPRITE::TRANSLUCENT);
		}
	}
	
	//11 Phoenix
	class Phoenix : Enemy {
		Phoenix(jjOBJ@ preset) {
			super(preset);
			preset.energy = 3;
			gender = Gender::Male;
			for (uint i = 0; i < 3; ++i) {
				Recolor(jjAnimations[jjAnimSets[ANIM::RAVEN] + i], array<uint8>={24}, RecolorReplace(88));
				Recolor(jjAnimations[jjAnimSets[ANIM::RAVEN] + i], array<uint8>={72}, array<uint8>={0,0,0,0,46,44,42,41});
			}
		}
		void onBehave(jjOBJ@ obj) override {
			if (obj.state != STATE::DONE) {
				const auto oldDirection = obj.direction;
				for (uint i = (obj.state == STATE::ATTACK) ? 3 : IsMurderer ? 2 : 1; i > 0; --i)
					obj.behave(BEHAVIOR::RAVEN, false);
				if (oldDirection != obj.direction) {
					jjSample(obj.xPos, obj.yPos, SOUND::AMMO_FIREGUN2A);
					obj.direction *= -1;
					for (int i = 0; i < 3; ++i) {
						jjOBJ@ bullet = jjObjects[jjAddObject(OBJECT::TOASTERBULLET, obj.xPos + i * 15 * oldDirection, obj.yPos + (i-1) * 10)];
						bullet.playerHandling = HANDLING::ENEMYBULLET;
						bullet.state = STATE::FLY;
						bullet.animSpeed = 1;
						bullet.xSpeed /= 3;
						bullet.xAcc /= 2;
						bullet.yAcc = (int(jjRandom() & 3) - 1) / 64.f; //replicates normal spreading behavior for player-fired toaster
					}
					obj.direction *= -1;
				}
				if (obj.state == STATE::STOP)
					obj.state = STATE::IDLE; //never return to origin place
			}
		}
		void onDraw(jjOBJ@ obj) override {
			if (obj.state != STATE::ATTACK)
				Enemy::onDraw(obj);
			else
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::SINGLEHUE, 40);
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) override {
			if (obj.state == STATE::ATTACK) {
				if (bullet is null)
					player.hurt(IsMurderer ? 2 : 1);
				return true;
			}
			return Enemy::onObjectHit(obj,bullet,player,force);
		}
	}
	
	class Demon : Enemy {
		Demon(jjOBJ@ preset) {
			super(preset);
			for (uint i = 0; i < 4; ++i) {
				Recolor(jjAnimations[jjAnimSets[ANIM::DEMON] + i], array<uint8>={24}, RecolorReplace(48));
				Recolor(jjAnimations[jjAnimSets[ANIM::DEMON] + i], array<uint8>={64,80}, RecolorReplace(72));
			}
		}
		void onBehave(jjOBJ@ obj) override {
			obj.special = (obj.state == STATE::IDLE) ? obj.counter : 0;
			
			if (obj.state != STATE::DONE) {
				if (obj.state == STATE::START) {
					obj.isBlastable = getParameterAtOrigin(obj, -2, 1) == 1;
					obj.lightType = LIGHT::NONE;
				}
				if (obj.special > 140) obj.special = 140;
				else if (obj.state == STATE::WALK && abs(obj.direction) == 1)
					obj.direction *= 2;
				if (!obj.isBlastable) { //rightside up
					obj.behave(BEHAVIOR::DEMON, false);
				} else { //upside down
					obj.yPos -= 64;
					Player.yPos -= 64;
					obj.behave(BEHAVIOR::DEMON, false);
					obj.yPos += 64;
					Player.yPos += 64;
				}
				if (obj.special != 0 && obj.state == STATE::ATTACK) {
					FireLoop(obj, true);
				}
			}
		}
		void onDraw(jjOBJ@ obj) override {
			if (obj.isBlastable) obj.direction ^= SPRITE::FLIPV;
			Enemy::onDraw(obj);
			if (obj.isBlastable) obj.direction ^= SPRITE::FLIPV;
			FireLoop(obj, false);
		}
		void FireLoop(const jjOBJ@ obj, bool attack) const {
			const int fireFrame = jjAnimations[jjAnimSets[ANIM::AMMO] + 13] + ((jjGameTicks >> 2) % 7);
			for (int i = 0; i + 16 < obj.special; i += (IsMurderer ? 16 : 32)) {
				const int angle = (i + jjGameTicks) << 3;
				const int distance = (200 - obj.special) / 2;
				const float xPos = obj.xPos + jjSin(angle) * distance;
				const float yPos = obj.yPos + jjCos(angle) * distance;
				if (attack) {
					jjOBJ@ spark = jjObjects[jjAddObject(OBJECT::SHARD, xPos, yPos, obj.objectID)];
					spark.determineCurAnim(ANIM::AMMO, 13);
					spark.animSpeed = 1;
					spark.playerHandling = HANDLING::ENEMYBULLET;
					spark.xSpeed /= 2;
					spark.lightType = LIGHT::RING2;
					spark.light = 2;
				} else {
					jjDrawSpriteFromCurFrame(xPos, yPos, fireFrame);
					if (jjAnimFrames[Player.curFrame].doesCollide(int(Player.xPos), int(Player.yPos), Player.direction, jjAnimFrames[fireFrame], int(xPos), int(yPos), 1))
						Player.hurt(IsMurderer ? 2 : 1);
					for (uint j = jjObjectCount; --j != 0;) {
						jjOBJ@ bullet = jjObjects[j];
						if (bullet.playerHandling == HANDLING::PLAYERBULLET && bullet.freeze == 0 && bullet.isActive && jjAnimFrames[bullet.curFrame].doesCollide(int(bullet.xPos), int(bullet.yPos), bullet.direction, jjAnimFrames[fireFrame], int(xPos), int(yPos), 1)) {
							bullet.particlePixelExplosion(1);
							bullet.delete();
						}
					}
				}
			}
		}
	}
	
	class Duet : Enemy {
		Duet(jjOBJ@ preset) {
			super(preset, AnimSets::Duet);
			Layer = 3;
			preset.isFreezable = false; //splitness
			preset.special = jjAnimations[jjAnimSets[animSetID]];
			preset.lightType = LIGHT::BRIGHT;
			preset.light = 16;
			preset.direction = 1;
		}
		void onBehave(jjOBJ@ obj) override {
			if (obj.state == STATE::DONE)
				return;
			if (obj.state == STATE::START || obj.state == STATE::FREEZE || obj.state == STATE::DEACTIVATE) {
				if (obj.state == STATE::START)
					obj.xAcc = float(jjRandom() & 1023);
				obj.behave(BEHAVIOR::SPARK, false);
				return;
			} //else...
			obj.behave(BEHAVIOR::SPARK, false);
			if (!obj.isFreezable) { //not yet split
				obj.doesHurt = jjSampleLooped(obj.xPos,obj.yPos,SOUND::COMMON_SHIELD_ELEC,obj.doesHurt);
				obj.xAcc += (obj.xSpeed) * (IsMurderer ? 7 : 5) + obj.direction; //angle
				const int angle = int(obj.xAcc);
				for (int i = 0; i < 1024; i += 512) {
					const float xPos = obj.xPos + jjSin(angle+i) * 50, yPos = obj.yPos + jjCos(angle+i) * 50;
					const jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
					if (jjAnimFrames[Player.curFrame].doesCollide(int(Player.xPos), int(Player.yPos), Player.direction, frame, int(xPos), int(yPos), 1)) {
						const auto force = Player.getObjectHitForce();
						if (force == 0)
							Player.hurt(1);
						else
							split(obj, xPos, yPos, null, force);
					}
					for (uint j = jjObjectCount; --j != 0;) {
						jjOBJ@ bullet = jjObjects[j];
						if (bullet.playerHandling == HANDLING::PLAYERBULLET && bullet.freeze == 0 && bullet.isActive && jjAnimFrames[bullet.curFrame].doesCollide(int(bullet.xPos), int(bullet.yPos), bullet.direction, frame, int(xPos), int(yPos), 1)) {
							split(obj, xPos, yPos, bullet, 0);
						}
					}
					if (obj.isFreezable) { //got split
						i ^= 512;
						obj.xPos += jjSin(angle+i) * 50;
						obj.yPos += jjCos(angle+i) * 50;
						obj.lightType = LIGHT::NONE;
						break;
					}
				}

[preview ends here]