Downloads containing HH18_Guardian.j2as

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Holiday Hare '18Featured Download SmokeNC Single player 8.9 Download file

File preview

#include "MLLE-Include-1.4.asc"
const bool MLLESetupSuccessful = MLLE::Setup();
#pragma require "HH18_Guardian-MLLE-Data-2.j2l"
#pragma require "Castle2.j2t"
#pragma require "HH18_Guardian-MLLE-Data-1.j2l"
#pragma require "Damn1.j2t"
#pragma require "HH18_Guardian.j2l"
#pragma require "HH17_lowind.wav"
#pragma require "HH17_Glass1.wav"
#pragma require "HH18E1.j2a"
#include "HH18Smoke.asc"
#include "HH18savegems.asc"

enum EJStates {Intro, Run, Jump, Buttstomp, Uppercut, Hurt, EJ_Die, EJ_End};
uint currEJState = Intro;

enum DevanStates {Devan_Intro, Warp_Out, Warp_In, Device};
uint currDevanState = Devan_Intro;

enum DRAKStates {Hidden, Reveal, Freeze_Devan, Fade_Out, Awaiting, Begin, Fly, Icicles, Freeze_Cloud, Ice_Bomb, Summon_Enemies, Homing_Ice, Raining_Icicles, Icicle_Burst, Crystals, Complete};
uint currDRAKState = Hidden;

bool cutscene = false;
uint scene, elapsed, count = 0;
bool snowblind = false;
bool transition = false;
bool climax = false;
bool outro = false;
bool done = false;
int sample = 0;

int healthFactor = 4+jjDifficulty;

bool limitScrollBeforeDrak = false;
bool refresh = false;

const float PI = 3.1415927f;

bool facingPlayer(jjOBJ@ obj, jjPLAYER@ play) {
	if (((obj.xPos < (play.xPos - 32) && obj.direction == 1) || (obj.xPos > (play.xPos + 32) && obj.direction == -1)) && obj.yPos <= int(play.yPos + 32) && obj.yPos >= int(play.yPos - 32)) return true;
	return false;
}

void onLevelLoad() {
	uint src = jjAnimSets[ANIM::CUSTOM[255]].load(0, "SExmas.j2a");
	uint dest = jjAnimSets[ANIM::PICKUPS];
	for (int i = 0; i < 95; i++) {
		const jjANIMATION@ anim = jjAnimations[src + i];
		if (anim.frameCount != 0)
			jjAnimations[dest + i] = anim;
	}
	jjAnimSets[ANIM::BRIDGE].load(1, "SExmas.j2a");
	jjAnimSets[ANIM::CUSTOM[0]].load(2, "SExmas.j2a");

	jjAnimSets[ANIM::CUSTOM[10]].load(13, "HH18E1.j2a");
	jjAnimSets[ANIM::CUSTOM[11]].load(0, "HH18E1.j2a");
	jjAnimSets[ANIM::CUSTOM[12]].load(8, "HH18E1.j2a");
	jjAnimSets[ANIM::CUSTOM[13]].load(12, "HH18E1.j2a");
	jjUseLayer8Speeds = true;

	jjObjectPresets[OBJECT::EGGPLANT].behavior = EvilJazz();
	jjObjectPresets[OBJECT::EGGPLANT].playerHandling = HANDLING::SPECIAL;
	jjObjectPresets[OBJECT::EGGPLANT].bulletHandling = HANDLING::DETECTBULLET;
	jjObjectPresets[OBJECT::EGGPLANT].scriptedCollisions = true;
	jjObjectPresets[OBJECT::EGGPLANT].isTarget = true;
	jjObjectPresets[OBJECT::EGGPLANT].isBlastable = false;
	jjObjectPresets[OBJECT::EGGPLANT].energy = 12 + (3*jjDifficulty);
	jjObjectPresets[OBJECT::EGGPLANT].lightType = LIGHT::PLAYER;
	jjObjectPresets[OBJECT::EGGPLANT].light = 12;
	jjObjectPresets[OBJECT::EGGPLANT].deactivates = false;
	jjObjectPresets[OBJECT::EGGPLANT].points = 5000;
	
	jjObjectPresets[OBJECT::WEENIE].determineCurAnim(ANIM::CUSTOM[10], 0);
	jjObjectPresets[OBJECT::WEENIE].behavior = Drak();
	jjObjectPresets[OBJECT::WEENIE].playerHandling = HANDLING::SPECIAL;
	jjObjectPresets[OBJECT::WEENIE].bulletHandling = HANDLING::DETECTBULLET;
	jjObjectPresets[OBJECT::WEENIE].scriptedCollisions = true;
	jjObjectPresets[OBJECT::WEENIE].isBlastable = false;
	jjObjectPresets[OBJECT::WEENIE].energy = 100;
	jjObjectPresets[OBJECT::WEENIE].age = 400 + (100*jjDifficulty);
	jjObjectPresets[OBJECT::WEENIE].deactivates = false;
	jjObjectPresets[OBJECT::WEENIE].points = 50000;
	
	jjObjectPresets[OBJECT::THING].determineCurAnim(ANIM::DEVILDEVAN, 1);
	jjObjectPresets[OBJECT::THING].behavior = Devan();
	jjObjectPresets[OBJECT::THING].playerHandling = HANDLING::PARTICLE;
	jjObjectPresets[OBJECT::THING].bulletHandling = HANDLING::IGNOREBULLET;
	jjObjectPresets[OBJECT::THING].isBlastable = false;
	jjObjectPresets[OBJECT::THING].deactivates = false;
	
	jjObjectPresets[OBJECT::CAKE].behavior = Portal();
	jjObjectPresets[OBJECT::CAKE].playerHandling = HANDLING::SPECIAL;
	jjObjectPresets[OBJECT::CAKE].bulletHandling = HANDLING::IGNOREBULLET;
	jjObjectPresets[OBJECT::CAKE].scriptedCollisions = true;
	jjObjectPresets[OBJECT::CAKE].isBlastable = false;
	jjObjectPresets[OBJECT::CAKE].deactivates = false;
	
	jjObjectPresets[OBJECT::STEADYLIGHT].behavior = SnowEffect();
	
	jjAnimSets[ANIM::DEVAN].load();
	jjAnimSets[ANIM::DEVILDEVAN].load();
	jjAnimSets[ANIM::HATTER].load();
	jjAnimSets[ANIM::ROBOT].load();
	
	jjSampleLoad(SOUND::SCIENCE_PLOPKAOS, "HH17_lowind.wav");
	jjSampleLoad(SOUND::FAN_FAN, "HH17_Glass1.wav");
	
	SMOKE::FROZENSHADE(OBJECT::HATTER,2+jjDifficulty);
	
	gem::restorePlayerGems();
	
}

void onLevelReload() {
	MLLE::Palette.apply();
	jjMusicLoad("cracking_ice.mod");
	currEJState = Intro;
	currDevanState = Devan_Intro;
	cutscene = transition = climax = false;
	refresh = true;
	gem::restorePlayerGems();
}

void onLevelBegin() {
	for (uint i = 0; i < 5; i++) {
		jjANIMATION@ animDemon = jjAnimations[jjAnimSets[ANIM::CUSTOM[10]] + i];
		for (uint j = 0; j < animDemon.frameCount; j++) {
			jjANIMFRAME@ frame = jjAnimFrames[animDemon + j];
			jjPIXELMAP sprite(frame);
			for (uint x = 0; x < sprite.width; ++x) {
				for (uint y = 0; y < sprite.height; ++y) {
					if (sprite[x,y] >= 16 && sprite[x,y] <= 23) sprite[x,y] += 15;
					if (sprite[x,y] >= 48 && sprite[x,y] <= 55) sprite[x,y] += 23;
					if (i == 4) {
						if (j == 0) {
							if (x >= 29 || y >= 53) sprite[x,y] = 0;
						}
						if (j == 1) {
							if (y >= 51) sprite[x,y] = 0;
						}
						if (j == 2) {
							if (y >= 53) sprite[x,y] = 0;
						}
					}
				}
			}
			sprite.save(frame);
		}
	}
	
	for (uint i = 0; i < 5; i++) {
		jjANIMATION@ animHatter = jjAnimations[jjAnimSets[ANIM::HATTER] + i];
		for (uint j = 0; j < animHatter.frameCount; j++) {
			jjANIMFRAME@ frame = jjAnimFrames[animHatter + j];
			jjPIXELMAP sprite(frame);
			for (uint x = 0; x < sprite.width; ++x) {
				for (uint y = 0; y < sprite.height; ++y) {
					sprite[x,y] = 254;
				}
			}
			sprite.save(frame);
		}
	}
	
	jjANIMATION@ animHatter = jjAnimations[jjAnimSets[ANIM::ROBOT] + 0];
	for (uint j = 0; j < animHatter.frameCount; j++) {
		jjANIMFRAME@ frame = jjAnimFrames[animHatter + j];
		jjPIXELMAP sprite(frame);
		for (uint x = 0; x < sprite.width; ++x) {
			for (uint y = 0; y < sprite.height; ++y) {
				if (sprite[x,y] >= 72 && sprite[x,y] <= 79) {
					sprite[x,y] -= 40;
				}
			}
		}
		sprite.save(frame);
	}
	
	for (int i = 1; i < jjObjectCount; i++) {
		if (jjObjects[i].eventID == OBJECT::HATTER) {
			jjObjects[i].delete();
		}
	}
}

class EvilJazz : jjBEHAVIORINTERFACE {
	int currAnim, fireRate, freezeTime, stompTime, hurtTime, blinkTime;
	bool jump, blinking, dead;
	
	void onBehave(jjOBJ@ obj) {
		obj.determineCurAnim(ANIM::JAZZ, currAnim);
		obj.determineCurFrame();
		
		if (jjGameTicks % 7 == 0 && obj.freeze == 0 && currEJState != EJ_End) obj.frameID++;
		
		if (!blinking) {
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, obj.freeze > 0? SPRITE::FROZEN : SPRITE::PLAYER, 31);
		} else {
			if (jjGameTicks % 10 / 5 == 0) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, obj.freeze > 0? SPRITE::FROZEN : SPRITE::PLAYER, 31);
		}
		
		for (int i = 0; i <= jjLocalPlayerCount; i++) {
			jjLocalPlayers[i].boss = obj.objectID;
		}
		
		int playerID = obj.findNearestPlayer(500000);
		jjPLAYER@ play = jjPlayers[playerID];
		
		obj.xPos = obj.xPos + obj.xSpeed;
		obj.yPos = obj.yPos + obj.ySpeed;
		
		switch (jjDifficulty) {
			case 0: fireRate = 28; break;
			case 1: fireRate = 21; break;
			case 2: fireRate = 14; break;
			case 3: fireRate = 7;
		}
		
		if (obj.var[0] == 1) {
			if (obj.energy > 0) {
				if (!blinking) currEJState = Hurt;
			} else {
				currEJState = EJ_Die;
				obj.frameID = 0;
				obj.var[0] = 0;
				jjMusicStop();
				jjSample(play.xPos, play.yPos, SOUND::INTRO_BOEM1, 0, 32500);
			}
			
			if (blinking) {
				obj.freeze = 0;
				blinkTime++; 
				if (blinkTime == 140) {
					obj.var[0] = 0;
					blinkTime = 0;
					blinking = false;
				}
			}
		}
		
		if (obj.freeze == 0) {
			freezeTime = 0;
			switch (currEJState) {
				case Intro:
					scene = 1;
					obj.var[0] = 0;
					currAnim = RABBIT::STAND;
					blinking = false;
					blinkTime = 0;
					obj.direction = -1;
					for (int i = 0; i < jjLocalPlayerCount; i++) {
						jjLocalPlayers[i].limitXScroll(125,25);
					}
				break;
				case Run:
					if (obj.xSpeed == 0) {
						currAnim = (play.keyDown && (!play.keyLeft || play.keyRight))? RABBIT::DIVEFIRERIGHT : RABBIT::FIRE;
					} else {
						if ((obj.direction == 1 && obj.xSpeed > 4) || (obj.direction == -1 && obj.xSpeed < -4)) {
							currAnim = RABBIT::RUN2;
						} else {
							currAnim = RABBIT::RUN1;
						}
					}
					obj.putOnGround(false);
					if (obj.yPos > obj.yOrg) obj.yPos = obj.yOrg;
					if (playerID >= -1) {
						if (obj.freeze == 0) {
							if ((play.xPos < obj.xPos - 256) || (play.xPos > obj.xPos + 256)) {
								accelerate(obj, 6*obj.direction);
							} else if (facingPlayer(obj, play)) {
								decelerate(obj, 0);
							}
							shoot(obj, play);
							if (play.xPos > obj.xPos + 48) obj.direction = 1;
							else if (play.xPos < obj.xPos - 48) obj.direction = -1;
							
							if (((play.yPos < int(obj.yPos - 96)) || ((play.charCurr == CHAR::SPAZ || play.charCurr == CHAR::LORI) && play.specialMove > 7)) && !jump) {
								if ((obj.xPos < int(play.xPos + 64) && obj.xPos > int(play.xPos - 64))) {
									currEJState = Uppercut;
									stompTime = 0;
								} else {
									currEJState = Jump;
									jump = true;
									jjSample(obj.xPos, obj.yPos, SOUND::COMMON_JUMP);
								}
							}
							if ((obj.xPos < int(play.xPos + 48) && obj.xPos > int(play.xPos - 48)) && play.ySpeed == 0) {
								currEJState = Jump;
								jump = true;
								jjSample(obj.xPos, obj.yPos, SOUND::COMMON_JUMP);
							}
						}
					}
				break;
				case Jump:
					if (obj.ySpeed < -8.5) obj.ySpeed = -8.5;
					if (playerID >= -1) {
						if ((play.xPos < obj.xPos - 96) || (play.xPos > obj.xPos + 96)) accelerate(obj, 5.5*obj.direction);
						else if (facingPlayer(obj, play)) decelerate(obj, 0);
						if (play.xPos > obj.xPos + 48) obj.direction = 1;
						else if (play.xPos < obj.xPos - 48) obj.direction = -1;
						obj.ySpeed = obj.ySpeed + 0.225;
						
						if (play.buttstomp == 41 && obj.yPos > play.yPos) obj.xSpeed = -2*obj.direction;
							
						if (!jump && obj.ySpeed > 0 && obj.yPos > int(obj.yOrg - 10)) {
							obj.putOnGround(false);
							obj.ySpeed = 0;
							obj.yPos = obj.yOrg;
							jjSample(obj.xPos, obj.yPos, SOUND::COMMON_LANDPOP);
							currEJState = Run;
						}
						
						if (!jump && !blinking && (obj.xPos < int(play.xPos + 8) && obj.xPos > int(play.xPos - 8)) && obj.yPos < play.yPos && obj.yPos > int(play.yPos - 224)) {
							obj.xSpeed = obj.ySpeed = 0;
							currEJState = Buttstomp;
							stompTime = 0;
						}
						
						if ((play.xPos < obj.xPos - 224) || (play.xPos > obj.xPos + 224)) {
							currAnim = obj.ySpeed < 0? RABBIT::ROLLING : RABBIT::RIGHTFALL;
						} else {
							shoot(obj, play);
							currAnim = RABBIT::JUMPFIRERIGHT;
						}
						
						if (jump) {
							obj.ySpeed = play.ySpeed == 0? -6 : (int(play.yPos-obj.yPos)/10);
							jump = false;
						}
					}
				break;
				case Buttstomp:
					stompTime++;
					if (stompTime < 16 && obj.ySpeed == 0) {
						currAnim = RABBIT::SPRING;
					}
					else {
						currAnim = RABBIT::FALLBUTTSTOMP;
						if (stompTime == 16) {
							stompTime = 0;
							jjSample(obj.xPos, obj.yPos, SOUND::COMMON_DOWN, 0, 32000);
						}
						obj.ySpeed = 8;
						if (obj.yPos > int(obj.yOrg - 8)) {
							obj.putOnGround(false);
							obj.ySpeed = 0;
							obj.yPos = obj.yOrg;
							jjSample(obj.xPos, obj.yPos, SOUND::COMMON_LANDPOP);
							currEJState = Run;
						}
					}
				break;
				case Uppercut:
					stompTime++;
					obj.xSpeed = 0;
					if (stompTime < 14) {
						currAnim = jjIsTSF? 64:79;
					} else if (stompTime > 14 && stompTime < 40) {
						currAnim = jjIsTSF? 62:77;
						obj.ySpeed = -7;
					} else if (stompTime > 40) {
						currEJState = Jump;
						stompTime = 0;
					}
				break;
				case Hurt:
					currAnim = RABBIT::HURT;
					obj.freeze = 0;
					if (!blinking) hurtTime++;
					if (hurtTime == 1) {
						randomHurtSounds(obj, jjRandom()%5);
					}
					if (hurtTime == 30) {
						blinking = true;
						hurtTime = 0;
						currEJState = Jump;
					} else if (hurtTime > 0) {
						obj.xSpeed = -2 * obj.direction;
						if (hurtTime < 14) obj.ySpeed = -2.5;
					}
				break;
				case EJ_Die:
					currAnim = RABBIT::DIE;
					obj.xSpeed = 0;
					obj.freeze = 0;
					if (obj.yPos > int(obj.yOrg - 8)) {
						obj.putOnGround(false);
						obj.yPos = obj.yOrg;
						obj.ySpeed = 0;
					} else obj.ySpeed = 4;
					if (obj.frameID >= 12) currEJState = EJ_End;
				break;
				case EJ_End:
					obj.freeze = 0;
					if (obj.frameID == 13 && jjGameTicks % 7 == 0) jjSample(play.xPos, play.yPos, SOUND::COMMON_BENZIN1);
					if (obj.frameID < 19) {
						if (jjGameTicks % 7 == 0) obj.frameID++;
					} else currAnim = RABBIT::CORPSE;
				break;
			}
		} else {
			obj.xSpeed = obj.ySpeed = 0;
			freezeTime++;
			if (freezeTime == 60) {
				obj.unfreeze(1);
			}
		}
		if (obj.xPos < 124*32) obj.xPos = 124*32;
		if (obj.xPos > 151*32) obj.xPos = 151*32;
	}
	
	void shoot(jjOBJ@ obj, jjPLAYER@ play) {
		if (jjGameTicks % fireRate == 0) {
			jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::BLASTERBULLET)];
			bullet.killAnim = jjObjectPresets[OBJECT::BLASTERBULLET].killAnim;
			bullet.playerHandling = HANDLING::ENEMYBULLET;
			bullet.state = STATE::FLY;
			bullet.animSpeed = 1;
			jjSample(obj.xPos, obj.yPos, SOUND::AMMO_GUNJAZZ);
		}
	}
	
	void accelerate(jjOBJ@ obj, float speed) {
		if (obj.direction == 1 && obj.xSpeed < speed) obj.xSpeed += 0.25;
		else if (obj.direction == -1 && obj.xSpeed > speed) obj.xSpeed -= 0.25;
	}
	
	void decelerate(jjOBJ@ obj, float speed) {
		if (obj.xSpeed > speed) obj.xSpeed -= 0.1;
		else if (obj.xSpeed < speed) obj.xSpeed += 0.1;
		
		if ((obj.direction == 1 && obj.xSpeed < 1) || (obj.direction == -1 && obj.xSpeed > -1)) {
			obj.xSpeed = 0;
		}
	}
	
	void randomHurtSounds(jjOBJ@ obj, int random) {
		switch(random) {
			case 0: jjSample(obj.xPos, obj.yPos, SOUND::JAZZSOUNDS_JAZZV1, 0, 10000); break;
			case 1: jjSample(obj.xPos, obj.yPos, SOUND::JAZZSOUNDS_JAZZV2, 0, 10000); break;
			case 2: jjSample(obj.xPos, obj.yPos, SOUND::JAZZSOUNDS_JAZZV3, 0, 10000); break;
			case 3: jjSample(obj.xPos, obj.yPos, SOUND::JAZZSOUNDS_JAZZV4, 0, 10000); break;
			case 4: jjSample(obj.xPos, obj.yPos, SOUND::JAZZSOUNDS_HEY4, 0, 10000); break;
		}
	}
	
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bull, jjPLAYER@ play, int force) {
		if (bull !is null) {
			if (bull.playerHandling == HANDLING::PLAYERBULLET && obj.var[0] == 0 && currEJState < EJ_Die && currEJState != Buttstomp && currEJState != Intro) {
				obj.justHit = 5;
				obj.energy -= (bull.var[6] == 8? 2:1);
				obj.var[0] = 1;
				if (obj.freeze > 0) {
					obj.unfreeze(1);
				}
			}
			if ((bull.var[6] & 16) == 0 && currEJState < EJ_Die) {
				bull.state = STATE::EXPLODE;
			}
		} else if (play !is null) {
			if (obj.var[0] == 0 && play.blink == 0 && force == 0 && currEJState < EJ_Die && (currEJState != Buttstomp || currEJState != Uppercut)) {
				play.xAcc = 0;
				play.xSpeed /= -2;
				if (play.yPos < obj.yPos) play.ySpeed = -6;
			}
			if (currEJState == Buttstomp && obj.ySpeed > 0 && obj.yPos < play.yPos - 8 && !blinking) {
				play.hurt(2, false);
				obj.ySpeed = -8;
				currEJState = Jump;
			}
			if (currEJState == Uppercut && play.yPos < obj.yPos && !blinking) {
				play.hurt(2, play.buttstomp < 41 && play.blink == 0? true:false);
			}
			if (force != 0 && obj.var[0] == 0 && play.blink == 0 && currEJState < EJ_Die) {
				obj.energy -= 2;
				obj.var[0] = 1;
				if (obj.freeze > 0) {
					obj.unfreeze(1);
				}
				if (force > 0) {
					play.buttstomp = 50;
					play.ySpeed = play.ySpeed / -2 - 8;
					play.yAcc = 0;
					play.extendInvincibility(-70);
				} else if (force == -101) {
					play.xAcc = 0;
					play.xSpeed /= -2;
					play.ySpeed = -6;
					play.extendInvincibility(-10);
				}
			}
		}
		return true;
	}
}

class Devan : jjBEHAVIORINTERFACE {
	ANIM::Set currSet;
	int currAnim, warpTime;
	bool draw;
	
	void onBehave(jjOBJ@ devan) {
		devan.determineCurAnim(currSet, currAnim);
		devan.determineCurFrame();
		
		if (jjGameTicks % 7 == 0 && devan.state != STATE::FREEZE) {
			if (currAnim == 6 || currAnim == 10) devan.frameID--;
			else devan.frameID++;
		}
		
		if (devan.var[0] == 1) devan.state = STATE::FREEZE;
		
		switch (currDevanState) {
			case Devan_Intro:
				currSet = ANIM::DEVILDEVAN;
				currAnim = 8;
				devan.direction = -1;
				if (cutscene && !outro) {
					draw = true;
					if (elapsed == 10) {
						jjAlert("|||||||Devan: Atchoo! I have a cold, so I don't feel like fighting you right now.");
					}
					if (elapsed == 210) jjAlert("|||||||Devan: So I'll just have Evil Jazz do it for me. He's a bit bitey today...");
					if (elapsed == 430) jjAlert("|||||||Devan: He'll keep you busy while I go get the device. Ho ho!");
					if (elapsed == 630) {
						cutscene = false;
						devan.frameID = 7;
						jjSamplePriority(SOUND::DEVILDEVAN_LAUGH);
						jjSamplePriority(SOUND::COMMON_TELPORT1);
						currDevanState = Warp_Out;
						warpTime = 0;
						currEJState = Run;
					}
				} else draw = false;
			break;
			case Warp_Out:
				currAnim = 10;
				warpTime++;
				if (warpTime > 49 && currEJState < EJ_End) {
					draw = false;
				}
				if (currEJState == EJ_End) {
					if (warpTime == 210) {
						for (int i = 0; i < jjObjectCount; i++) {
							if (jjObjects[i].eventID == OBJECT::EGGPLANT) {
								for (int j = 0; j < jjLocalPlayerCount; j++) {
									givePlayerPointsForObject(jjLocalPlayers[j], jjObjects[i]);
									jjLocalPlayers[j].bossActivated = false;
								}
								jjObjects[i].particlePixelExplosion(0);
								jjObjects[i].delete();
							}
						}
					}
					if (warpTime == 350) {
						draw = true;
						cutscene = true;
						jjSamplePriority(SOUND::COMMON_TELPORT2);
						jjSamplePriority(SOUND::COMMON_BUBBLGN1);
						currDevanState = Warp_In;
						warpTime = 0;
						devan.frameID = 6;
						jjMusicLoad("Menur.mod");
					}
				} else if (currEJState == EJ_Die) {
					warpTime = 0;
				}
			break;
			case Warp_In:
				currSet = ANIM::DEVAN;
				currAnim = 6;
				warpTime++;
				for (int i = 0; i < jjLocalPlayerCount; i++) {
					jjLocalPlayers[i].direction = devan.xPos > jjLocalPlayers[i].xPos? 1:-1;
					devan.direction = devan.xPos > jjLocalPlayers[i].xPos? -1:1;
					if (devan.xPos > int(jjLocalPlayers[i].xPos - 24) && devan.xPos < int(jjLocalPlayers[i].xPos + 24)) {
						jjLocalPlayers[i].xSpeed = -10;
						jjLocalPlayers[i].ySpeed = -3;
					}
				}
				if (warpTime == 49) {
					currDevanState = Device;
				}
			break;
			case Device:
				currAnim = 1;
				if (cutscene && !outro) {
					if (elapsed == 70) jjAlert("|||||||Devan: Your adventure ends here, Jackrabbit!");
					if (elapsed == 280) jjAlert("|||||||Devan: Once I enable this device, my winter machine will activate at full power!");
					if (elapsed == 490) jjAlert("|||||||Devan: At a push of a button, your pitiful world will be frozen for all eternity!");
					if (elapsed == 700) jjAlert("|||||||Devan: Surrender now, or else Carrotus is finished!");
					if (elapsed == 1190) jjAlert("|||||||Devan: Another silent protagonist, huh? Alright, prepare to freeze!");
					if (elapsed == 1470) {
						jjSamplePriority(SOUND::PINBALL_FLIP1);
						jjMusicStop();
					}
					if (elapsed == 1680) {
						jjSamplePriority(SOUND::PINBALL_FLIP1);
					}
					if (elapsed == 1750) {
						jjSamplePriority(SOUND::PINBALL_FLIP1);
					}
					if (elapsed > 1750 && elapsed < 2100 && elapsed % 14 == 0) {
						jjSamplePriority(SOUND::PINBALL_FLIP1);
					}
					if (elapsed == 1998) jjAlert("|||||||Devan: Stupid hunk of junk! Why won't you work?");
					if (elapsed == 2170) {
						jjSamplePriority(SOUND::PINBALL_FLIP1);
						snowblind = true;
						for (int i = 0; i < jjLocalPlayerCount; i++) {
							jjLocalPlayers[i].lighting = 255;
						}
					}
					if (elapsed == 2730) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~I HAVE BEEN @@@LIBERATED...", STRING::LARGE);
						for (int i = 0; i < jjLocalPlayerCount; i++) {
							jjLocalPlayers[i].lighting = 80;
							jjSetDarknessColor(jjPALCOLOR(64,128,255));
							jjLocalPlayers[i].xPos = (136*32)+(16*i);
							jjLocalPlayers[i].direction = 1;
						}
						devan.direction = -1;
					}
					if (elapsed == 3080) {
						scene = 2;
						currDRAKState = Reveal;
						jjLocalPlayers[0].showText("@@@@@@#Who are you?");
					}
					if (elapsed == 4310) {
						jjAlert("|||||||Devan: My winter machine was using only a fraction of your full power.");
					}
					if (elapsed == 4520) {
						jjAlert("|||||||Devan: I have used the machine to shatter your crystal and hence release you.");
					}
					if (elapsed == 4730) {
						jjAlert("|||||||Devan: You may call me master. Now, freeze that rabbit, my servant!");
					}
				}
			break;
		}
		
		if (draw) jjDrawSpriteFromCurFrame(devan.xPos, devan.yPos, devan.curFrame, devan.direction, devan.state == STATE::FREEZE? SPRITE::FROZEN : SPRITE::NORMAL);
		devan.putOnGround(true);
	}
}

class Drak : jjBEHAVIORINTERFACE {
	int currAnim, currLeftHandAim, currRightHandAim;
	int delayTime, attackTime, attackDelay, randcar, position;
	bool draw, firstAttack;
	
	void onBehave(jjOBJ@ obj) {
		obj.determineCurAnim(ANIM::CUSTOM[10], currAnim);
		obj.determineCurFrame();
		
		obj.counter++;
		
		if (jjGameTicks % 7 == 0 && currDRAKState < Complete) {
			obj.frameID++;
		}
		
		obj.energy = int(obj.age / healthFactor);
		if (obj.energy <= 0) {
			currDRAKState = Complete;
		}
		
		attackDelay = obj.energy < 50? 95:175;
		
		switch (jjDifficulty) {
			case 0: randcar = 25; break;
			case 1: randcar = 50; break;
			case 2: randcar = 100; break;
			default: randcar = 100; break;
		}
		
		int playerID = obj.findNearestPlayer(1000000);
		jjPLAYER@ play = jjPlayers[playerID];
		
		obj.xPos = obj.xPos + obj.xSpeed;
		obj.yPos = obj.yPos + obj.ySpeed;
		
		if (currDRAKState > Awaiting) {
			for (int i = 0; i <= jjLocalPlayerCount; i++) {
				jjLocalPlayers[i].boss = obj.objectID;
			}
		}
		
		if (draw) {
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, obj.justHit > 0? SPRITE::SINGLECOLOR : SPRITE::NORMAL, 15);
			if (obj.direction == 1) {
				jjDrawSprite(obj.xPos + 9, obj.yPos - 9, ANIM::CUSTOM[10], 4, currLeftHandAim, obj.direction, obj.justHit > 0? SPRITE::SINGLECOLOR : SPRITE::NORMAL, 15);
				jjDrawSprite(obj.xPos - 24, obj.yPos - 11, ANIM::CUSTOM[10], 3, currRightHandAim, obj.direction, obj.justHit > 0? SPRITE::SINGLECOLOR : SPRITE::NORMAL, 15);
			} else if (obj.direction == -1) {
				jjDrawSprite(obj.xPos - 9, obj.yPos - 9, ANIM::CUSTOM[10], 4, currLeftHandAim, obj.direction, obj.justHit > 0? SPRITE::SINGLECOLOR : SPRITE::NORMAL, 15);
				jjDrawSprite(obj.xPos + 24, obj.yPos - 11, ANIM::CUSTOM[10], 3, currRightHandAim, obj.direction, obj.justHit > 0? SPRITE::SINGLECOLOR : SPRITE::NORMAL, 15);
			}
			obj.isTarget = true;
		}
		
		if (play.xPos > int(obj.xPos+32)) obj.direction = 1;
		else if (play.xPos < int(obj.xPos-32)) obj.direction = -1;
		
		int flightAngle = int(atan2(obj.xPos - int(play.xPos + (128*obj.direction)), obj.yPos - int(play.yPos - 160)) * (512 / PI));
		int attackAngle = int(atan2(obj.xPos - play.xPos, obj.yPos - play.yPos) * (512 / PI));
		
		obj.freeze = 0;
		
		switch (currDRAKState) {
			case Hidden:
				draw = false;
				currAnim = 0;
			break;
			case Reveal:
				draw = true;
				obj.direction = -1;
				currLeftHandAim = 0;
				currRightHandAim = 1;
				position = jjResolutionHeight <= 480? 63*32 : 60*32;
				if (obj.yPos < position) {
					obj.ySpeed = 1;
				} else {
					obj.ySpeed = 0;
				}
				if (jjResolutionHeight <= 480) obj.xPos = obj.xOrg - 16;
				obj.isTarget = true;
				if (cutscene) {
					if (elapsed == 3400) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~I AM AN@@@ICE DEMON.", STRING::LARGE);
					}
					if (elapsed == 3750) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~I HAVE BEEN FREED@@@FROM MY PRISON.", STRING::LARGE);
					}
					if (elapsed == 4100) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~WHO HAS@@@SUMMONED ME?", STRING::LARGE);
					}
					if (elapsed == 5080) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~YOU SHALL@@@PERISH, FOOL!", STRING::LARGE);
					}
					if (elapsed == 5220) {
						currDRAKState = Freeze_Devan;
					}
					if (elapsed == 5480) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~SO YOU ARE THE@@@ONE WHO OPPOSES ME...", STRING::LARGE);
					}
					if (elapsed == 5830) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~VERY WELL.@@@YOU INTRIGUE ME.", STRING::LARGE);
					}
					if (elapsed == 6180) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~HOWEVER, I CANNOT@@@ALLOW YOU TO LIVE.", STRING::LARGE);
					}
					if (elapsed == 6530) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~I WANT TO START@@@A NEW ICE AGE.", STRING::LARGE);
					}
					if (elapsed == 6880) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~AN ICE AGE THAT@@@LASTS FOREVER.", STRING::LARGE);
					}
					if (elapsed == 7230) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~AS YOU DEFY ME,@@@I AM FORCED@@@TO DESTROY YOU.", STRING::LARGE);
					}
					if (elapsed == 7580) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~HOWEVER, I WILL@@@LET YOU FIGHT ME@@@AT FULL POWER.", STRING::LARGE);
					}
					if (elapsed == 7930) {
						jjLocalPlayers[0].showText("@@@@@@@@@#|||||~STEP INTO THE PORTAL@@@AND CONFRONT YOUR FATE.", STRING::LARGE);
						currDRAKState = Fade_Out;
					}
				}
			break;
			case Freeze_Devan:
				if (elapsed < 5410 && elapsed % 8 == 0) {
					currLeftHandAim = 2;
					currRightHandAim = 0;
					IceCloud temp;
					for (int i = 0; i < 2; i++) {
						jjOBJ@ freeze = jjObjects[jjAddObject(OBJECT::BULLET, i == 0? int(obj.xPos - 32): int(obj.xPos + 16), i == 0? int(obj.yPos - 9) : int(obj.yPos - 11), obj.objectID, CREATOR::OBJECT, jjVOIDFUNCOBJ(temp.onBehave))];
						freeze.state = STATE::FLY;
						freeze.playerHandling = HANDLING::ENEMYBULLET;
						freeze.counterEnd = 210;
						freeze.xAcc = 0;
						for (int j = 1; j < jjObjectCount; j++) {
							jjOBJ@ devan = jjObjects[j];
							if (devan.eventID == OBJECT::THING) {
								int devanAngle = int(atan2(freeze.xPos - devan.xPos, freeze.yPos - devan.yPos) * (512 / PI));
								freeze.xSpeed = -5 * jjSin(devanAngle);
								freeze.ySpeed = -5 * jjCos(devanAngle);
								jjSamplePriority(SOUND::AMMO_ICEGUN);
							}
						}
					}
				} else if (elapsed > 5410) {
					currDRAKState = Reveal;
				}
			break;
			case Fade_Out:
				if (elapsed >= 7930 && elapsed <= 8335) {
					draw = false;
					if (elapsed % 8 == 0) {
						jjOBJ@ glitter = jjObjects[jjAddObject(OBJECT::EXPLOSION, int(obj.xPos - 40) + jjRandom()%80, int(obj.yPos + 40) - jjRandom()%80)];
						glitter.determineCurAnim(ANIM::PICKUPS, 86);
					}
				} else if (elapsed > 8335) {
					cutscene = false;
					obj.delete();
				}
			break;
			case Awaiting:
				draw = false;
				delayTime = attackTime = 0;
				transition = true;
				firstAttack = false;
			break;
			case Begin:
				currLeftHandAim = 0;
				currRightHandAim = 1;
				draw = true;
				obj.direction = -1;
				obj.yPos += jjCos(obj.counter*6);
				delayTime++;
				if (delayTime == 70) {
					snowblind = transition = false;
					climax = true;
					currDRAKState = Fly;
					jjMusicLoad("final_transition.xm");
					delayTime = attackTime = 0;
				}
			break;
			case Fly:
				obj.yPos += jjCos(obj.counter*6);
				moveIntoXPosition(obj, play, 3, flightAngle);
				moveIntoYPosition(obj, play, 2, flightAngle);
				
				attackTime++;
				
				if (attackTime >= attackDelay) {
					if (!firstAttack) {
						firstAttack = true;
						attackTime = 0;
						currDRAKState = Icicles;
					}
					else {
						randomAttack(obj);
					}
				}
				
				currLeftHandAim = 0;
				currRightHandAim = 1;
			break;
			case Icicles:
				attackStance(obj);
				if (attackTime >= 35 && attackTime < 280) {
					obj.ySpeed = -0.25;
					if (attackTime % 28 == 0) {
						Icicle temp;
						for (int i = 0; i < 2; i++) {
							jjOBJ@ icicle = jjObjects[jjAddObject(OBJECT::BLASTERBULLET, i == 0? int(obj.xPos - (32*obj.direction)): int(obj.xPos + (16*obj.direction)), i == 0? int(obj.yPos - 9) : int(obj.yPos - 11), obj.objectID, CREATOR::OBJECT, jjVOIDFUNCOBJ(temp.onBehave))];
							icicle.determineCurAnim(ANIM::CUSTOM[11], 2);
							icicle.state = STATE::FLY;
							icicle.playerHandling = HANDLING::ENEMYBULLET;
							icicle.counterEnd = 210;
							icicle.xSpeed = -6 * jjSin(attackAngle);
							icicle.ySpeed = -6 * jjCos(attackAngle);
							jjSample(obj.xPos, obj.yPos, SOUND::AMMO_ICEPU2);
						}
					}
				} else if (attackTime == 280) {
					currDRAKState = Fly;
					attackTime = 0;
				}
			break;
			case Freeze_Cloud:
				attackStance(obj);
				if (attackTime >= 35 && attackTime < 280) {
					obj.xSpeed = -2 * jjSin(attackAngle);
					obj.ySpeed = -1.5 * jjCos(attackAngle);
					if (attackTime % 10 == 0) {
						IceCloud temp;
						for (int i = 0; i < 2; i++) {
							jjOBJ@ freeze = jjObjects[jjAddObject(OBJECT::BULLET, i == 0? int(obj.xPos - (32*obj.direction)): int(obj.xPos + (16*obj.direction)), i == 0? int(obj.yPos - 9) : int(obj.yPos - 11), obj.objectID, CREATOR::OBJECT, jjVOIDFUNCOBJ(temp.onBehave))];
							freeze.state = STATE::FLY;
							freeze.playerHandling = HANDLING::SPECIAL;
							freeze.scriptedCollisions = true;
							freeze.counterEnd = 210;
							freeze.xAcc = 0;
							freeze.xSpeed = -4 * jjSin(attackAngle);
							freeze.ySpeed = -4 * jjCos(attackAngle);
							jjSample(obj.xPos, obj.yPos, SOUND::AMMO_ICEGUN);
						}
					}
				} else if (attackTime == 280) {
					currDRAKState = Fly;
					attackTime = 0;
				}
			break;
			case Ice_Bomb:
				attackStance(obj);
				obj.yPos += jjCos(obj.counter*4);
				moveIntoXPosition(obj, play, 1, flightAngle);
				moveIntoYPosition(obj, play, 0.5, flightAngle);
				if (attackTime >= 35 && attackTime < 350) {
					if (attackTime % 65 == 0) {
						IceBomb temp;
						jjOBJ@ bomb = jjObjects[jjAddObject(OBJECT::BLASTERBULLET, obj.xPos, int(obj.yPos - 10), obj.objectID, CREATOR::OBJECT, jjVOIDFUNCOBJ(temp.onBehave))];
						bomb.determineCurAnim(ANIM::CUSTOM[12], 2);
						bomb.state = STATE::FLY;
						bomb.playerHandling = HANDLING::ENEMYBULLET;	
						bomb.counterEnd = 60;
						bomb.direction = obj.direction;
						bomb.xSpeed = -4 * jjSin(attackAngle);
						bomb.ySpeed = -3 - abs(jjRandom()%2);
						bomb.xAcc = 0.05*obj.direction;
						bomb.yAcc = 0.15;
						jjSample(obj.xPos, obj.yPos, SOUND::COMMON_SWISH6);
					}
				} else if (attackTime == 350) {
					currDRAKState = Fly;
					attackTime = 0;
				}
			break;
			case Summon_Enemies:
				attackStance(obj);
				if (attackTime == 35) {
					jjOBJ@ spawn = jjObjects[jjAddObject(OBJECT::EXPLOSION, int(obj.xPos + (96 * obj.direction)), int(obj.yPos + 32), obj.objectID, CREATOR::OBJECT)];
					spawn.determineCurAnim(ANIM::PICKUPS, 86);
					spawn.frameID = 0;
					jjSample(spawn.xPos, spawn.yPos, SOUND::COMMON_ITEMTRE);
					
					jjOBJ@ enemy = jjObjects[jjAddObject(OBJECT::HATTER, int(obj.xPos + (96 * obj.direction)), int(obj.yPos + 38), obj.objectID, CREATOR::OBJECT)];
					enemy.determineCurAnim(ANIM::CUSTOM[23], 0);
					enemy.determineCurFrame();
					enemy.energy = 2+jjDifficulty;
					enemy.frameID = 1;
					enemy.state = STATE::START;
					currDRAKState = Fly;
					attackTime = 0;
				}
			break;
			case Homing_Ice:
				attackStance(obj);
				obj.yPos += jjCos(obj.counter*4);
				if (attackTime == 70) {
					HomingIce temp;
					jjOBJ@ ice = jjObjects[jjAddObject(OBJECT::BLASTERBULLET, int(obj.xPos + (64*obj.direction)), int(obj.yPos - 10), obj.objectID, CREATOR::OBJECT, jjVOIDFUNCOBJ(temp.onBehave))];
					ice.determineCurAnim(ANIM::ROBOT, 0);
					ice.state = STATE::FLY;
					ice.playerHandling = HANDLING::ENEMYBULLET;	
					ice.bulletHandling = HANDLING::DESTROYBULLET;
					ice.counterEnd = 255;
					ice.direction = obj.direction;
					ice.xAcc = ice.yAcc = 0;
					jjSample(obj.xPos, obj.yPos, SOUND::AMMO_ICEPU4);
					currDRAKState = Fly;
					attackTime = 0;
				}
			break;
			case Raining_Icicles:
				attackStance(obj);
				obj.yPos += jjCos(obj.counter*6);
				if (attackTime == 104) jjSamplePriority(SOUND::FAN_FAN);
				if (attackTime >= 105 && attackTime < 350) {
					if (jjRandom()%randcar < (randcar - 1)) {
						if (attackTime % 4 == 0) {
							Icicle temp;
							jjOBJ@ icicle = jjObjects[jjAddObject(OBJECT::BLASTERBULLET, (3712 + jjRandom()%1056), 12*32, obj.objectID, CREATOR::OBJECT, jjVOIDFUNCOBJ(temp.onBehave))];
							icicle.xSpeed = 0;
							icicle.ySpeed = 6;
							icicle.direction = 1;
							icicle.counterEnd = 200;
							icicle.state = STATE::FLY;
							icicle.playerHandling = HANDLING::ENEMYBULLET;
						}
					} else {
						if (attackTime % 4 == 0) {
							jjOBJ@ carrot = jjObjects[jjAddObject(OBJECT::CARROT, (3712 + jjRandom()%1056), 12*32, obj.objectID, CREATOR::OBJECT)];
							carrot.state = STATE::FLOATFALL;
						}
					}
				} else if (attackTime == 350) {
					currDRAKState = Fly;
					attackTime = 0;
				}
			break;
			case Icicle_Burst:
				attackStance(obj);
				if (attackTime >= 55 && attackTime < 350) {
					if (play.xPos > int(obj.xPos - 96) && play.xPos < int(obj.xPos + 96)) obj.xSpeed = -1 * obj.direction;
					moveIntoYPosition(obj, play, 0.75, flightAngle);
					if (obj.xPos > 150*32) obj.xPos = 150*32;
					obj.yPos += jjCos(obj.counter*4);
					if (attackTime % 70 == 0) {
						Icicle temp;
						for (int i = 0; i < 5; i++) {
							jjOBJ@ icicle = jjObjects[jjAddObject(OBJECT::BLASTERBULLET, int(obj.xPos + (32*obj.direction)), int(obj.yPos - 10), obj.objectID, CREATOR::OBJECT, jjVOIDFUNCOBJ(temp.onBehave))];
							icicle.determineCurAnim(ANIM::CUSTOM[11], 2);
							icicle.state = STATE::FLY;
							icicle.playerHandling = HANDLING::ENEMYBULLET;
							icicle.counterEnd = 210;
							icicle.xSpeed = 7 * obj.direction;
							icicle.ySpeed = i == 0? -3 : i == 1? -1 : i == 2? 0 : i == 3? 1 : 3;
							jjSample(obj.xPos, obj.yPos, SOUND::AMMO_ICEPU2);
						}
					}
				} else if (attackTime == 350) {
					currDRAKState = Fly;
					attackTime = 0;
				}
			break;
			case Crystals:
				attackStance(obj);
				sample = jjSampleLooped(play.xPos, play.yPos, SOUND::COMMON_AIRBOARD, sample, 25000, 0);
				if (attackTime > 70 && attackTime < 700) {
					if (attackTime % 28 == 0) {
						IceCrystal temp;
						jjOBJ@ crystal = jjObjects[jjAddObject(OBJECT::BULLET, play.xPos, 35*32, obj.objectID, CREATOR::OBJECT, jjVOIDFUNCOBJ(temp.onBehave))];
						crystal.xSpeed = 0;
						crystal.ySpeed = -2.75;
						crystal.direction = 1;
						crystal.state = STATE::FLY;
						crystal.playerHandling = HANDLING::ENEMYBULLET;
						crystal.bulletHandling = HANDLING::DESTROYBULLET;
					}
				} else if (attackTime == 700) {
					currDRAKState = Fly;
					attackTime = 0;
				}
			break;
			case Complete:
				obj.xSpeed = obj.ySpeed = 0;
				outro = true;
				if (elapsed == 10) {
					for (int i = 0; i < jjLocalPlayerCount; i++) {
						givePlayerPointsForObject(jjLocalPlayers[i], obj);
					}
				}
				if (elapsed >= 350) {
					jjDrawResizedSprite(int(obj.xPos - (16*obj.direction)), obj.yPos, ANIM::CUSTOM[13], 8, 0, 2, 2, SPRITE::TRANSLUCENT);
				}
			break;
		}
	}
	
	void moveIntoXPosition(jjOBJ@ obj, jjPLAYER@ play, float speed, int angle) {
		if (obj.xPos > int(play.xPos + 140) || obj.xPos < int(play.xPos - 140)) {
			obj.xSpeed = -speed * jjSin(angle);
		} else {
			obj.xSpeed = 0;
		}
	}
	void moveIntoYPosition(jjOBJ@ obj, jjPLAYER@ play, float speed, int angle) {
		if (obj.yPos < int(play.yPos - 176) || obj.yPos > int(play.yPos - 64)) {
			obj.ySpeed = -speed * jjCos(angle);
		} else {
			obj.ySpeed = 0;
		}
	}
	void attackStance(jjOBJ@ obj) {
		currLeftHandAim = 2;
		currRightHandAim = 0;
		attackTime++;
		if (attackTime < 35) obj.xSpeed = obj.ySpeed = 0;
	}
	void randomAttack(jjOBJ@ obj) {
		if (obj.energy >= 50) {
			currDRAKState = getNextAttack(highHPQueue, highHPAttacks);
		} else {
			currDRAKState = getNextAttack(lowHPQueue, lowHPAttacks);
		}
		attackTime = 0;
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bull, jjPLAYER@ play, int force) {
		if (bull !is null) {
			if (bull.playerHandling == HANDLING::PLAYERBULLET && currDRAKState > Begin) {
				obj.justHit = 5;
				if (bull.var[3] == 5) {
					obj.age -= 1;
				} else {
					obj.age -= bull.animSpeed;
				}
			}
			if ((bull.var[6] & 16) == 0) {
				bull.state = STATE::EXPLODE;
			}
		} else if (play !is null) {
			play.hurt(1, false);
		}
		return true;
	}
}

class IceCloud : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.counter++;
		
		for (int i = 1; i < jjObjectCount; i++) {
			if (jjObjects[i].eventID == OBJECT::THING && obj.doesCollide(jjObjects[i], true)) {
				jjObjects[i].var[0] = 1;
				obj.delete();
			}
		}
		
		int playerID = obj.findNearestPlayer(1000);
		if (playerID > -1) {
			if (obj.doesCollide(jjPlayers[playerID], true)) jjPlayers[playerID].freeze(true);
		}
	
		obj.xPos = obj.xPos + obj.xSpeed;
		obj.yPos = obj.yPos + obj.ySpeed;
		
		if (obj.counter == int(obj.counterEnd)) obj.delete();
		
		obj.determineCurAnim(ANIM::AMMO, 82);
		obj.determineCurFrame();
		if (jjGameTicks % 7 == 0) obj.frameID++;
		
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::FROZEN);
	}
}

class Icicle : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::BULLET, false);
		obj.var[0] = int(atan2(-obj.ySpeed, obj.xSpeed) * (512.f * 0.318309886142228f));
		if (obj.state == STATE::EXPLODE) {
			obj.unfreeze(1);
			obj.delete();
		} else {
			obj.xAcc = 0;
			obj.yAcc = 0;
			jjDrawRotatedSprite(obj.xPos, obj.yPos, ANIM::CUSTOM[11], 2, 0, obj.var[0], 1, 1, SPRITE::NORMAL);
		}
	}
}

class IceBomb : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::BULLET, true);
		if (obj.state == STATE::EXPLODE && obj.isActive) {
			obj.unfreeze(1);
			 for (int i = 0; i < 8; i++) {
				int bulletID3 = jjAddObject(OBJECT::BLASTERBULLET, obj.xPos, obj.yPos, obj.objectID, CREATOR::OBJECT);
				jjOBJ @ o = jjObjects[bulletID3];
				o.determineCurAnim(ANIM::CUSTOM[12], 3);
				o.direction = obj.direction;
				o.ySpeed = 3*sin(i*PI/4);
				o.yAcc=0;
				o.xAcc=0;
				o.xSpeed = 3 * cos(i*PI/4);
				o.killAnim = jjObjectPresets[OBJECT::ICEBULLET].killAnim;
				o.playerHandling = HANDLING::ENEMYBULLET;
				o.counterEnd = 90;
			}
			obj.delete();
		}
	}
}

class HomingIce : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::BULLET, false);

		int playerID = obj.findNearestPlayer(1000000);
		int angle = int(atan2(obj.xPos - jjPlayers[playerID].xPos, obj.yPos - jjPlayers[playerID].yPos) * (512 / PI));
		
		obj.xSpeed = -4 * jjSin(angle);
		obj.ySpeed = -4 * jjCos(angle);
		
		if (obj.state == STATE::EXPLODE) {
			obj.unfreeze(1);
			obj.delete();
		} else {
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::NORMAL);
		}
	}
}

class IceCrystal : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.determineCurAnim(ANIM::CUSTOM[13], 8);
		obj.determineCurFrame();
		
		obj.yPos = obj.yPos + obj.ySpeed;
		if (obj.yPos < 10*32) obj.delete();

		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::TRANSLUCENT);
	}
}

const array<DRAKStates> highHPAttacks = {Icicles, Freeze_Cloud, Ice_Bomb, Summon_Enemies, Homing_Ice};
const array<DRAKStates> lowHPAttacks = {Icicles, Freeze_Cloud, Ice_Bomb, Summon_Enemies, Homing_Ice, Raining_Icicles, Icicle_Burst, Crystals};

array<DRAKStates> highHPQueue;
array<DRAKStates> lowHPQueue;

void shuffle(array<DRAKStates>& input) {
    for (int i = input.length(); i != 0;) {
        int index = jjRandom() % i;
        i--;
        DRAKStates temp = input[index];
        input[index] = input[i];
        input[i] = temp;
    }
}

DRAKStates pop(array<DRAKStates>& input) {
    DRAKStates result = input[input.length() - 1];
    input.removeLast();
    return result;
}

DRAKStates getNextAttack(array<DRAKStates>& queue, const array<DRAKStates>& reserve) {
    if (queue.isEmpty()) {
        queue = reserve;
        shuffle(queue);
    }
    return pop(queue);
}

class Portal : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.yPos = currDRAKState == Fade_Out? obj.yOrg:78*32;
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bull, jjPLAYER@ play, int force) {
		if (play !is null) {
			if (obj.var[0] == 0) {
				play.warpToID(1, true);
				play.cameraFreeze(78*32, jjResolutionHeight <= 480? 15*32 : 12*32, false, true);
				play.limitXScroll(78, 75);
				play.xSpeed = 0;
				play.ySpeed = 4;
				obj.var[0] = 1;
				jjSamplePriority(SOUND::COMMON_TELPORT2);
			}
		}
		return true;
	}
}

bool givePlayerPointsForObject(jjPLAYER@ player, jjOBJ@ obj) { //This will probably be made a jjOBJ method as part of the real JJ2+ API eventually, because it shows up all the time in the native code, but that hasn't happened yet, so here you go. Increases the player's jjPLAYER::score to match the object's jjOBJ::points, and creates a string particle with that number which flies up to the top left corner of the screen.
	if (player is null)
		return false;
	if (obj.points != 0 && (jjGameMode == GAME::SP || jjGameMode == GAME::COOP)) {
		player.score += obj.points;
		jjPARTICLE@ particle = jjAddParticle(PARTICLE::STRING);
		if (particle !is null) {
			particle.xPos = obj.xPos;
			particle.yPos = obj.yPos;
			particle.xSpeed = (-32768 - int(jjRandom() & 0x3FFF)) / 65536.f;
			particle.ySpeed = (-65536 - int(jjRandom() & 0x7FFF)) / 65536.f;
			particle.string.text = formatInt(obj.points);
		}
		obj.points = 0; //Ensures that JJ2 won't end up increasing the player's score TWICE, since a call to (the native version of) this function is actually part of the standard pickup-handling code and would otherwise therefore get you in trouble with the FiveUp onObjectHit code.
		return true;
	}
	return false;
}

class SnowEffect : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::STEADYLIGHT);
		obj.deactivates = false;
		obj.light = snowblind? 127:-127;
		obj.xPos = jjLocalPlayers[0].cameraX + 400;
		obj.yPos = jjLocalPlayers[0].cameraY + 300;
	}
}

void onPlayer(jjPLAYER@ play) {
	gem::trackPlayerGems(play);
	gem::upgradeHealth(play);
	if (cutscene) {
		play.keyLeft = play.keyRight = play.keyDown = play.keyJump = play.keyFire = play.keyRun = false;
		play.keyUp = scene == 2? true:false;
		play.idle = 0;
		jjCharacters[play.charCurr].canRun = false;
		elapsed++;
		play.invincibility = -7;
	} else {
		elapsed = 0;
		if (play.charCurr == CHAR::JAZZ || play.charCurr == CHAR::SPAZ || play.charCurr == CHAR::LORI) jjCharacters[play.charCurr].canRun = true;
	}
	gem::trackPlayerGems(play);
	gem::upgradeHealth(play);
	
	array<jjLAYER@> layers = jjLayerOrderGet();
	
	if (snowblind) {
		play.lightType = LIGHT::NONE;
		sample = jjSampleLooped(play.xPos, play.yPos, SOUND::SCIENCE_PLOPKAOS, sample, 32, 0);
		for (uint i = 0; i < layers.length(); i++) {
			if (i != 15) layers[i].hasTiles = false;
			else layers[i].hasTiles = true;
		}
		jjMusicStop();
	} else {
		play.lightType = LIGHT::PLAYER;
		play.light = 12;
		if (climax) {
			for (uint i = 0; i < layers.length(); i++) {
				if (i != 16) layers[i].hasTiles = false;
				else layers[i].hasTiles = true;
			}
		} else {
			for (uint i = 0; i < layers.length(); i++) {
				if (i == 15) layers[i].hasTiles = false;
				else layers[i].hasTiles = true;
			}
		}
		jjSetDarknessColor(jjPALCOLOR(0,0,0));
	}
	
	if (transition && currDRAKState == Begin) {
		play.cameraUnfreeze(false);
		play.cameraFreeze(true, jjResolutionHeight <= 480? 15*32 : 12*32, false, true);
	}
	
	if (climax) play.lighting = 100;
	
	if (outro) {
		jjMusicStop();
		cutscene = true;
		scene = 0;
		if (elapsed == 10) {
			climax = false;
			play.lighting = 255;
		}
		if (elapsed == 70) {
			play.showText("@@@@@@@@@#|||||~ARGHHH!!!", STRING::LARGE);
		}
		if (elapsed == 350) {
			done = true;
			outro = false;
			play.lighting = 60;
			play.showText("@@@@@@#The Ice Demon has been re-sealed.@Carrotus is saved!");
		}
		if (elapsed == 700) {
			jjNxt();
		}
	}
	
	if (currDRAKState == Fade_Out && play.yPos < 30*32) {
		currDRAKState = Awaiting;
		play.cameraFreeze(true, false, false, true);
	}
	
	if (currDRAKState < Begin && jjGameTicks > 1) {
		for (int i = 1; i < jjObjectCount; i++) {
			if (jjObjects[i].eventID == OBJECT::HATTER) {
				jjObjects[i].delete();
			}
		}
	}
	
	if (play.health == 0) { 
		limitScrollBeforeDrak = false;
		if (currDRAKState > Awaiting) currDRAKState = Awaiting;
	}
	
	if (refresh) {
		play.cameraFreeze(play.xOrg, play.yOrg, true, true);
		play.xPos = 5*32;
		play.yPos = 26*32;
		count++;
		if (count == 2) {
			play.cameraUnfreeze();
			play.xPos = play.xOrg;
			play.yPos = play.yOrg;
			refresh = false;
			count = 0;
			if (currDRAKState == Awaiting) {
				play.cameraFreeze(true, jjResolutionHeight <= 480? 15*32 : 12*32, false, true);
				if (!limitScrollBeforeDrak) {
					snowblind = true;
					jjSetDarknessColor(jjPALCOLOR(64,128,255));
					play.cameraFreeze(78*32, jjResolutionHeight <= 480? 15*32 : 12*32, false, true);
					play.cameraFreeze(true, jjResolutionHeight <= 480? 15*32 : 12*32, false, true);
					play.limitXScroll(78, 75);
					limitScrollBeforeDrak = true;
				}
				play.lighting = 80;
			} else {
				jjSetDarknessColor(jjPALCOLOR(0,0,0));
			}
		}
	}
	
	jjEnforceLighting = snowblind || transition || outro? LIGHT::COMPLETE : LIGHT::OPTIONAL;
}

bool onDrawLives(jjPLAYER@ play, jjCANVAS@ canvas)  { return true; }

void onMain() {
	jjPlayers[31].furSet(72, 40, 24, 32);
	
	jjTexturedBGTexture = climax? TEXTURE::WTF : TEXTURE::LAYER8;
	jjTexturedBGStyle = climax? TEXTURE::TUNNEL : TEXTURE::WARPHORIZON;
	jjLayerXSpeed[8] = climax? 0:0.05f;
	jjLayerXAutoSpeed[8] = climax? 0.2f:0;
	jjLayerYAutoSpeed[8] = climax? 0.4f:0;
	jjTexturedBGStars = climax? true:false;
	
	gem::deleteCollectedGems();
	gem::draw = false;
}

void onFunction0(jjPLAYER@ play) {
	if (currEJState == Intro) {
		cutscene = true;
		jjMusicLoad("boss.s3m");
		play.activateBoss(true);
		play.cameraFreeze(137.5*32, jjResolutionHeight <= 480? 69.75*32 : 66.75*32, true, false);
	}
}

void onFunction1(jjPLAYER@ play) {
	if (currDRAKState == Awaiting) {
		play.cameraFreeze(115*32, false, true, false);
		play.limitXScroll(115, 50);
		play.activateBoss(true);
		play.lighting = 255;
		currDRAKState = Begin;
	}
}