Downloads containing HH17_Guardian.j2as

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Holiday Hare '17Featured Download ShadowGPW Single player 8.8 Download file

File preview

#include "MLLE-Include-1.6.asc" ///@MLLE-Generated
const bool MLLESetupSuccessful = MLLE::Setup();
#pragma require "HH17_Guardian.j2l"
#pragma require "Icicle.j2a"
#pragma require "HH17_Dragon.j2a"
#pragma require "SEXmas.j2a"
#pragma require "HH17_Roar.wav"

bool dragonActivated = false;
enum dragonStates {Intro, Idle, Roar, Homing_Fire, Flamethrower, Flame_Burst, Firebombs, Spawn_FireOrb, Shockwave};
uint currState = Intro;

int randcar = 20;
int frameRate = 7;
int shockSpeed = 3;
int healthFactor = 7;
int elapsed = 0;
bool charging = false;
bool defeated = false;

void singleColorLayer(jjLAYER@ layer, int color) {
	array<int> tileIDs, uniqueTileIDs;
	for (int i = 0; i < layer.height; i++) {
		for (int j = 0; j < layer.width; j++) {
			int tileID = layer.tileGet(j, i);
			if (tileID != 0)
				tileIDs.insertLast(tileID);
		}
	}
	int prev = 0;
	tileIDs.sortAsc();
	for (uint i = 0; i < tileIDs.length(); i++) {
		if (tileIDs[i] != prev)
			uniqueTileIDs.insertLast(prev = tileIDs[i]);
	}
	uint firstNewTile = jjTileCount;
	jjTilesFromTileset(jjTilesetFileName, 1, uniqueTileIDs.length());
	for (uint i = 0; i < uniqueTileIDs.length(); i++) {
		jjPIXELMAP tile(uniqueTileIDs[i]);
		for (int j = 0; j < 32; j++) {
			for (int k = 0; k < 32; k++) {
				uint8 pixel = tile[k, j];
				if (pixel != 0) {
					tile[k, j] = color;
				}
			}
		}
		tile.save(firstNewTile + i);
	}
	layer.generateSettableTileArea();
	for (int i = 0; i < layer.height; i++) {
		for (int j = 0; j < layer.widthReal; j++) {
			int tileID = layer.tileGet(j, i);
			if (tileID != 0)
				layer.tileSet(j, i, firstNewTile + uniqueTileIDs.find(tileID));
		}
	}
}

void onLevelLoad() {
	jjUseLayer8Speeds = true;
	jjTexturedBGFadePositionY = 0.52;
	
	singleColorLayer(jjLayers[6], 176);
	singleColorLayer(jjLayers[7], 204);
	
	jjAnimSets[ANIM::CUSTOM[0]].load(0, "HH17_Dragon.j2a");
	jjOBJ@ preset = jjObjectPresets[OBJECT::LIZARD];
	preset.determineCurAnim(ANIM::CUSTOM[0], 0);
	preset.behavior = DragonBoss();
	preset.energy = 100;
	preset.points = 50000;           
	preset.scriptedCollisions = true; 
	preset.playerHandling = HANDLING::SPECIAL;
	preset.bulletHandling = HANDLING::DETECTBULLET;
	preset.isBlastable = false;
	preset.isTarget = false;
	preset.special = 0;
	preset.animSpeed = 2;
	
	switch (jjDifficulty) {
		case 0: preset.age = 500; healthFactor = 5; break;
		case 1: preset.age = 700; healthFactor = 7; break;
		case 2: preset.age = 900; healthFactor = 9; break;
		default: preset.age = 900; healthFactor = 9; break;
	}
	
	jjAnimSets[ANIM::CUSTOM[2]].load(0, "HH17_Icicle.j2a");
	jjSampleLoad(SOUND::P2_FOEW1, "HH17_Glass2.wav");
	jjSampleLoad(SOUND::P2_FOEW4, "HH17_Glass3.wav");
	jjSampleLoad(SOUND::P2_FOEW5, "HH17_Glass4.wav");
	
	jjSampleLoad(SOUND::INTRO_MONSTER, "HH17_Roar.wav");
	
	jjAnimSets[ANIM::CUSTOM[1]].load(2, "SExmas.j2a");
	jjObjectPresets[OBJECT::BOMBCRATE].behavior = GiftBox();
	jjObjectPresets[OBJECT::BOMBCRATE].determineCurAnim(ANIM::CUSTOM[1], 0);
	
	jjObjectPresets[OBJECT::ICEBULLET].var[3] = jjObjectPresets[OBJECT::ICEBULLETPU].var[3] = 3;
}

void onLevelReload() {
	onLevelLoad();
	currState = Intro;
}

bool inView(const jjOBJ@ obj) {
	for (int i = 0; i < jjLocalPlayerCount; i++) {
		const jjPLAYER@ player = jjLocalPlayers[i];
		if (obj.xPos > player.cameraX - 64 && obj.yPos > player.cameraY - 64 && obj.xPos < player.cameraX + jjSubscreenWidth + 64 && obj.yPos < player.cameraY + jjSubscreenHeight + 64)
			return true;
	}
	return false;
}

void onLevelBegin() {
	jjPIXELMAP blackTile(243);
	for (uint x = 0; x < blackTile.width; ++x) {
		for (uint y = 0; y < blackTile.height; ++y) {
			blackTile[x,y] = 180;
		}
	}
	blackTile.save(243, true);
}

class GiftBox : jjBEHAVIORINTERFACE {
	void destroy(jjOBJ@ obj) {
		jjSample(obj.xPos, obj.yPos, SOUND::COMMON_WOOD1);
		{
			int id = jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos);
			if (id != 0) {
				jjOBJ@ other = jjObjects[id];
				other.determineCurAnim(ANIM::PICKUPS, 4);
			}
		}
		for (int i = jjRandom() & 7 | 8; i-- != 0;) {
			int id = jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos);
			if (id != 0) {
				jjOBJ@ other = jjObjects[id];
				other.determineCurAnim(ANIM::PICKUPS, 93 + (jjRandom() & 1));
			}
		}
		obj.yPos -= 8.f;
		for (int i = obj.var[1]; i-- != 0;) {
			int id = jjAddObject(obj.var[0], obj.xPos, obj.yPos);
			if (id != 0) {
				jjOBJ@ other = jjObjects[id];
				if (other.playerHandling == HANDLING::PICKUP) {
					int angle = (jjRandom() & 255) + 128;
					other.xSpeed = jjCos(angle) * 5.f;
					other.ySpeed = jjSin(angle) * -3.f;
				} else if (other.playerHandling == HANDLING::SPECIAL) {
					other.deactivates = false;
				}
			}
		}
		obj.clearPlatform();
		obj.delete();
	}
	void onBehave(jjOBJ@ obj) override {
		switch (obj.state) {
			case STATE::START:
				{
					uint16 x = int(obj.xOrg) >>> 5;
					uint16 y = int(obj.yOrg) >>> 5;
					obj.var[0] = jjParameterGet(x, y, 0, 8);
					obj.var[1] = jjParameterGet(x, y, 8, 4);
					obj.curAnim += jjParameterGet(x, y, 12, 2);
					obj.determineCurFrame();
					obj.bulletHandling = HANDLING::DESTROYBULLET;
					obj.scriptedCollisions = true;
				}
				break;
			case STATE::FALL:
				obj.var[2] = 1;
				break;
			case STATE::FREEZE:
			case STATE::SLEEP:
				if (obj.var[2] != 0) {
					destroy(obj);
					return;
				}
		}
		obj.behave(BEHAVIOR::MONITOR);
	}
	bool onObjectHit(jjOBJ@, jjOBJ@, jjPLAYER@, int) {
		return true;
	}
	bool onIsSolid(jjOBJ@) {
		return true;
	}
}

class DragonBoss : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.determineCurFrame();

		if (obj.freeze == 0 && obj.var[5] == 0) {
			if (jjGameTicks % frameRate == 0) obj.frameID++;
			obj.special++;
		}
		
		frameRate = obj.energy > 50? 7 : obj.energy < 25? 4 : 5;
		shockSpeed = obj.energy < 50? 4:3;
		
		if (obj.energy < 25 && obj.var[5] == 0) {
			dragonShout(obj, 36);
			
			if (obj.special % 250 == 0) jjSamplePriority(SOUND::INTRO_MONSTER);
		}
		
		if (obj.energy < 25 && obj.var[9] == 0) {
			jjLocalPlayers[0].showText("@@@@@@@Uh-oh! You've made the dragon very angry indeed!",  STRING::MEDIUM);
			obj.var[9] = 1;
		}
		
		if (obj.frameID > 20) obj.frameID = currState == Idle? 15:0;

		if (obj.state != STATE::KILL) {
			if (obj.var[5] == 0) {
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, obj.justHit > 0? SPRITE::SINGLECOLOR : obj.freeze > 0? SPRITE::FROZEN : SPRITE::SINGLEHUE, obj.justHit > 0? 15:71);
				if (obj.justHit == 0 && obj.freeze == 0 && jjColorDepth == 16) {
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::NEONGLOW, 3);
					if (charging && obj.special % 5 == 0) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::TRANSLUCENTCOLOR, 15);
					if (obj.energy < 75) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::NEONGLOW, 0);
					if (obj.energy < 50) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::NEONGLOW, 0);
					if (obj.energy < 25) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::NEONGLOW, 0);
				}
			} else if (obj.var[5] % 10 > 3) {
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, obj.justHit > 0? SPRITE::SINGLECOLOR : obj.freeze > 0? SPRITE::FROZEN : SPRITE::SINGLEHUE, obj.justHit > 0? 15:71);
				if (obj.justHit == 0 && obj.freeze == 0 && jjColorDepth == 16) {
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::NEONGLOW, 0);
					if (obj.energy < 75) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::NEONGLOW, 0);
					if (obj.energy < 50) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::NEONGLOW, 0);
					if (obj.energy < 25) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::NEONGLOW, 0);
				}
			}
		}
		obj.direction = 1;
		obj.yPos = obj.yOrg + 8;
		
		for (int i = 0; i < jjLocalPlayerCount; i++) {
			if (jjLocalPlayers[i].xPos > obj.xPos) jjLocalPlayers[i].xPos = obj.xPos;
		}
		
		if (obj.var[1] == 0) {
			jjOBJ@ target = jjObjects[jjAddObject(OBJECT::FLICKERLIGHT, obj.xPos - 32, obj.yPos - 140)];
			target.isTarget = true;
			target.light = 0;
			target.lightType = LIGHT::NONE;
			obj.var[1] = 1;
		}
		
		if (obj.state == STATE::FREEZE) {
			obj.var[10] = obj.var[10] - 1;
			if (obj.var[10] == 0) {
				obj.unfreeze(1);
				obj.state = obj.oldState;
			}
		} else {
			obj.var[10] = 105;
		}
		
		switch (jjDifficulty) {
			case 0: randcar = obj.energy < 25? 15:10; break;
			case 1: randcar = obj.energy < 25? 25:15; break;
			case 2: randcar = obj.energy < 25? 30:20; break;
			default: randcar = obj.energy < 25? 30:20; break;
		}
		
		switch (currState) {
			case Intro:
				for (int i = 0; i < jjLocalPlayerCount; i++) {
					if (inView(obj)) {
						obj.var[11] = 1;
						if (obj.frameID == 7 && obj.special % 7 == 0) {
							jjSamplePriority(SOUND::INTRO_MONSTER);
						}
					}
					if (obj.var[11] == 0) {
						obj.frameID = 0;
						obj.special = 0;
					}
					if (obj.special >= 185 && obj.frameID >= 15 && obj.frameID <= 20) {
						currState = Flamethrower;
						obj.special = 0;
					}
				}
			break;
			case Idle:
				if (obj.frameID < 15) obj.frameID = 15;
				if (obj.frameID == 20) obj.frameID = 15;
				
				if (obj.special >= 105) {
					randomAttack(obj);
				}
			break;
			case Roar:
				if (obj.frameID == 8 && obj.special % 7 == 1) {
					jjSamplePriority(SOUND::INTRO_MONSTER);
					jjSample(jjLocalPlayers[0].xPos, jjLocalPlayers[0].yPos, CollapsingIceSounds[jjRandom()%3]);
					obj.var[4] = 1;
				}
				if (obj.frameID >= 9 && obj.var[4] == 1) {
					dragonShout(obj, obj.energy < 50? 7:9);
					int playerID = obj.findNearestPlayer(100000);
					if (playerID >= -1) {
						jjPlayers[playerID].xSpeed = jjPlayers[playerID].xSpeed - 1;
					} 
				}
				
				if ((obj.special >= 70 && obj.frameID == 0) || obj.energy < 25) {
					randomAttack(obj);
					obj.var[4] = 0;
					jjLocalPlayers[0].cameraUnfreeze();
				}
				
			break;
			case Homing_Fire:
				if (obj.frameID >= 7 && obj.frameID <= 10) {	
					if (obj.counter < 7) {
						if (obj.counter % 7  == 0 && obj.freeze == 0 && obj.var[5] == 0) {
							jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::BULLET)];
							bullet.determineCurAnim(ANIM::BILSBOSS, 3);
							bullet.animSpeed = 1;
							bullet.behavior = HomingBullet();
							bullet.playerHandling = HANDLING::ENEMYBULLET;
							bullet.triggersTNT = true;
							bullet.isBlastable = true;
						}
							
						if (obj.counter == 1) {
							int playerID = obj.findNearestPlayer(40000);
							if (playerID >= -1) {
								jjSamplePriority(SOUND::DEVILDEVAN_DRAGONFIRE);
								jjSamplePriority(SOUND::BILSBOSS_FIRE);
							} 
						}
					}
					obj.counter++;
				}
				else obj.counter = 0;
				
				if (obj.special >= 140 && obj.frameID >= 15 && obj.frameID <= 20) {
					randomAttack(obj);
				}
			break;
			case Flamethrower:
				if (obj.frameID == 5 && obj.special % 7 == 0 && obj.var[5] == 0) jjSamplePriority(SOUND::BILSBOSS_FIRESTART);
				if (obj.frameID >= 7 && obj.frameID <= 10) {
					obj.var[4] = 1;
					int playerID = obj.findNearestPlayer(1000000);
					int sine = obj.energy > 50? 1: obj.energy < 25? 3:2;
					if (obj.counter % 1 == 0 && obj.freeze == 0 && obj.var[5] == 0) {
						jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::TOASTERBULLETPU)];
						bullet.behavior = Firebreather();
						bullet.lightType = LIGHT::POINT;
						bullet.playerHandling = HANDLING::ENEMYBULLET;
						bullet.animSpeed = 1;
						bullet.xSpeed = -16;
						float ys = jjRandom()%sine + (jjCos(obj.counter*32)) + jjSin(obj.counter*2)*2;
						bullet.ySpeed = ys;
						bullet.counterEnd = 165;
					}
					
						if (obj.counter == 1) {
							if (playerID >= -1) {
								jjSamplePriority(SOUND::BILSBOSS_FIRE);
							} 
						}
					
					if (obj.counter % 35 == 0 && obj.counter > 1 && obj.freeze == 0 && obj.var[5] == 0) jjSamplePriority(SOUND::COMMON_FLAMER);
					
					obj.counter++;
				}
				else obj.counter = 0;
				
				if (obj.var[4] == 1 && obj.special < 490 && obj.frameID > 9) obj.frameID = 7;
				
				if (obj.special >= 490 && obj.frameID >= 15 && obj.frameID <= 20) {
					randomAttack(obj);
					obj.var[4] = 0;
				}
			break;
			case Flame_Burst:
				if (obj.frameID >= 7 && obj.frameID <= 10 && obj.var[5] == 0) {
					obj.var[4] = 1;
					int playerID = obj.findNearestPlayer(1000000);
					int fireRate = obj.energy < 50? 6:10;
					if (obj.counter % fireRate == 0 && obj.freeze == 0) {
						jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::FIRESHIELDBULLET)];
						bullet.behavior = BEHAVIOR::BULLET;
						bullet.lightType = LIGHT::POINT;
						bullet.playerHandling = HANDLING::ENEMYBULLET;
						bullet.animSpeed = 1;
						bullet.xSpeed = -6;
						bullet.xAcc = 0;
						bullet.yAcc = 0;
						float ys = jjRandom()%4 + (jjCos(obj.counter*32));
						bullet.ySpeed = ys;
						bullet.counterEnd = 200;
					}
					
						if (obj.counter == 1) {
							if (playerID >= -1) {
								jjSamplePriority(SOUND::BILSBOSS_FIRE);
							} 
						}
					
					if (obj.counter % fireRate == 0 && obj.counter > 1 && obj.freeze == 0) jjSamplePriority(jjRandom()%2 > 0? SOUND::AMMO_FIREGUN1A : SOUND::AMMO_FIREGUN2A);
					
					obj.counter++;
				}
				else obj.counter = 0;
				
				if (obj.var[4] == 1 && obj.special < 350 && obj.frameID > 9) obj.frameID = 7;
				
				if (obj.special >= 350 && obj.frameID >= 15 && obj.frameID <= 20) {
					randomAttack(obj);
					obj.var[4] = 0;
				}
			break;
			case Firebombs:
				if (obj.frameID >= 7 && obj.frameID <= 10 && obj.var[5] == 0) {
					obj.var[4] = 1;
					int playerID = obj.findNearestPlayer(1000000);
					int fireRate = obj.energy < 50? 9:14;
					if (obj.counter % fireRate == 0 && obj.freeze == 0) {
						jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::FIRESHIELDBULLET)];
						jjSamplePriority(SOUND::BILSBOSS_FIRE);
						bullet.behavior = Firebomb();
						bullet.special = bullet.determineCurAnim(ANIM::BUBBA, 4);
						bullet.lightType = LIGHT::POINT;
						bullet.playerHandling = HANDLING::ENEMYBULLET;
						bullet.animSpeed = 1;
						bullet.xSpeed = -(4 + jjRandom()%3);
						bullet.ySpeed = -(6 + jjRandom()%3);
						bullet.xAcc = 0;
						bullet.yAcc = 0;
						bullet.counterEnd = 200;
						bullet.killAnim = jjObjectPresets[OBJECT::SEEKERBULLET].killAnim;
					}
					
						if (obj.counter == 1) {
							if (playerID >= -1) {
								jjSamplePriority(SOUND::BILSBOSS_FIRESTART);
							} 
						}
					
					obj.counter++;
				}
				else obj.counter = 0;
				
				if (obj.var[4] == 1 && obj.special < 350 && obj.frameID > 9) obj.frameID = 7;
				
				if (obj.special >= 350 && obj.frameID >= 15 && obj.frameID <= 20) {
					randomAttack(obj);
					obj.var[4] = 0;
				}
			break;
			case Spawn_FireOrb:
				if (obj.frameID >= 7 && obj.frameID <= 10 && obj.var[5] == 0) {	
					if (obj.counter < 7) {
						if (obj.counter % 7 == 0 && obj.freeze == 0) {
							jjOBJ@ orb = jjObjects[obj.fireBullet(OBJECT::SPARK)];
							orb.determineCurAnim(ANIM::AMMO, 77);
							orb.behavior = FireOrb();
							orb.playerHandling = HANDLING::SPECIAL;
							orb.bulletHandling = HANDLING::DETECTBULLET;
							orb.energy = 4;
							orb.triggersTNT = true;
							orb.isBlastable = true;
							orb.scriptedCollisions = true;
						}
							
						if (obj.counter == 1) {
							int playerID = obj.findNearestPlayer(40000);
							if (playerID >= -1) {
								jjSamplePriority(SOUND::DEVILDEVAN_DRAGONFIRE);
								jjSamplePriority(SOUND::BILSBOSS_FIRE);
							} 
						}
					}
					obj.counter++;
				}
				else obj.counter = 0;
				
				if (obj.special >= 140 && obj.frameID >= 15 && obj.frameID <= 20) {
					randomAttack(obj);
				}
			break;
			case Shockwave:
				if (obj.frameID >= 7 && obj.frameID <= 10 && obj.var[5] == 0) {
					if (obj.counter == 1 && obj.freeze == 0) {
						jjOBJ@ blast = jjObjects[obj.fireBullet(OBJECT::BULLET)];
						blast.behavior = DragonShockwave();
						blast.lightType = LIGHT::RING;
						blast.light = 100;
					}
					obj.counter++;
				}
				else obj.counter = 0;
				
				if (obj.special >= 140 && obj.frameID >= 15 && obj.frameID <= 20) {
					randomAttack(obj);
				}
			break;
		}

		if (obj.freeze == 0 && dragonActivated) obj.special++;

		obj.energy = int(obj.age / healthFactor);
		
		if (obj.animSpeed <= 0) {
			obj.age -= 1;
			obj.animSpeed = 2;
		}
		
		if (obj.age < 1) {
			obj.var[5] = obj.var[5] + 1;
			if (obj.var[5] < 350) {
				jjMusicStop();
				
				for (int i = 1; i < jjObjectCount; i++) {
					if (jjObjects[i].eventID == OBJECT::SPARK) jjObjects[i].state = STATE::KILL;
				}
				
				if (obj.var[5] == 10) {
					jjSample(jjLocalPlayers[0].xPos, jjLocalPlayers[0].yPos, SOUND::INTRO_MONSTER, 63, 45000);
					jjLocalPlayers[0].cameraUnfreeze(true);
				}
				if (obj.var[5] % 7 == 0 && obj.var[5] > 35) {
					jjOBJ@ boem = jjObjects[jjAddObject(OBJECT::EXPLOSION, int(obj.xPos - 130) + jjRandom()%260, int(obj.yPos) - jjRandom()%160)];
					boem.determineCurAnim(ANIM::AMMO, 5);
					jjSample(boem.xPos, boem.yPos, SOUND::COMMON_EXPL_TNT, 48, 0);
				}
			} else {
				for (int i = 0; i < 30; i++) {
					obj.particlePixelExplosion(1);
					obj.particlePixelExplosion(2);
				}
				for (int j = 0; j < jjLocalPlayerCount; j++) {
					givePlayerPointsForObject(jjLocalPlayers[j], obj);
					jjLocalPlayers[j].bossActivated = false;
				}
				jjLocalPlayers[0].showText("@@@@@@@@#||||~DRAGON SLAIN", STRING::LARGE);
				defeated = true;
				obj.delete();
			}
		} else obj.var[5] = 0;
		if (jjLocalPlayers[0].bossActivated) {
			jjLocalPlayers[0].boss = obj.objectID;
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bull, jjPLAYER@ play, int force) {
		if (bull !is null) {
			if (bull.playerHandling == HANDLING::PLAYERBULLET && obj.var[5] == 0) {
				if (bull.var[3] != 6) obj.age -= (bull.var[3] == 9? bull.animSpeed + 1 : bull.animSpeed) * ((bull.yPos < obj.yPos - 92) && (bull.xPos < obj.xPos + 8)? 2:1);
				else obj.animSpeed -= bull.animSpeed;
				obj.justHit = (bull.yPos < obj.yPos - 92) && (bull.xPos < obj.xPos + 8)? 7:5;
				if (obj.freeze > 0) {
					obj.unfreeze(1);
					obj.energy -= bull.animSpeed;
				}
			}
			if ((bull.var[6] & 16) == 0 || obj.var[5] == 0) {
				bull.state = STATE::EXPLODE;
			}
		} else if (play !is null) {
			if (obj.var[5] == 0) play.hurt();
			play.xSpeed = -6 * obj.direction;
		}
		return true;
	}
}

const array<SOUND::Sample> CollapsingIceSounds = {
	SOUND::P2_FOEW1,
	SOUND::P2_FOEW4,
	SOUND::P2_FOEW5
};

class Firebreather : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::TOASTERBULLET, false);
		if (obj.state != STATE::EXPLODE) {
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::PALSHIFT, 8);
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::ALPHAMAP, 25);
			obj.xPos -= 3;
		}
	}
}

class HomingBullet : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::BILSYBULLET);
		
		if (obj.counter > 70) obj.counter = 1;
		for (int i = 1; i < jjObjectCount; i++) {
			if (jjObjects[i].playerHandling == HANDLING::PLAYERBULLET && jjObjects[i].doesCollide(obj, true)) {
				jjObjects[i].state = STATE::EXPLODE;
			}
		}
	}
}

class Firebomb : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::BULLET, obj.state == STATE::EXPLODE? true:false);
		obj.var[0] = int(atan2(-obj.ySpeed, obj.xSpeed) * (512.f * 0.318309886142228f));
		if (obj.state != STATE::EXPLODE) {
			jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.var[0], 1, 1, SPRITE::NORMAL);
			if (obj.ySpeed < 8) obj.ySpeed += 0.15;
		}
	}
}

class FireOrb : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::SPARK, false);
		obj.special++;
		if (obj.special < 35) obj.xPos -= 4;
		else {
			obj.xPos += jjSin(obj.special*8);
			obj.yPos += jjCos(obj.special*8);
		}
		
		if (obj.energy < 1) obj.state = STATE::KILL;
		
		int carDropRate = jjDifficulty == 0? 1 : jjDifficulty == 1? 2 : 4;                                               
		
		if (obj.state != STATE::KILL) {
			jjDrawSprite(obj.xPos, obj.yPos, ANIM::AMMO, 77, 5, obj.direction, obj.justHit > 0? SPRITE::SINGLECOLOR : SPRITE::NORMAL, 15);
		} else {
			jjSample(obj.xPos, obj.yPos, SOUND::COMMON_BURN, 0, 0);
			obj.particlePixelExplosion(1);
			if (jjRandom()%carDropRate == 0) {
				jjOBJ@ carrot = jjObjects[jjAddObject(OBJECT::CARROT, int(obj.xPos), int(obj.yPos), obj.objectID, CREATOR::OBJECT)];
				carrot.state = STATE::FLOATFALL;
			}
			obj.delete();
		}
		
		int playerID = obj.findNearestPlayer(60000);
		if (playerID > -1) {
			if (obj.special % 60 == 0 && !jjMaskedPixel(int(obj.xPos), int(obj.yPos), 4)) {
				jjOBJ@ bullet = jjObjects[jjAddObject(OBJECT::FIRESHIELDBULLET, obj.xPos, obj.yPos)];
				bullet.xSpeed = jjPlayers[playerID].xPos > obj.xPos? 4:-4;
				bullet.state = STATE::FLY;
				bullet.playerHandling = HANDLING::ENEMYBULLET;
				bullet.animSpeed = 1;
				bullet.counterEnd = 105;
				bullet.xAcc = 0;
			}
		}
		
		for (int i = 1; i < jjObjectCount; i++) {
			if (jjObjects[i].var[3] == 3 && jjObjects[i].doesCollide(obj, true)) obj.state = STATE::KILL;
		}
		
		jjPARTICLE@ cinders = jjAddParticle(PARTICLE::SMOKE);
		cinders.xPos = int(obj.xPos - 8) + jjRandom()%16;
		cinders.yPos = obj.yPos;
		cinders.ySpeed = -0.5;
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bull, jjPLAYER@ play, int force) {
		if (bull !is null) {
			if (bull.playerHandling == HANDLING::PLAYERBULLET) {
				if (bull.var[3] != 6) {
					obj.energy -= bull.animSpeed;
					obj.justHit = 5;
				}
			}
			if ((bull.var[6] & 16) == 0) {
				bull.state = STATE::EXPLODE;
			}
		} else if (play !is null) {
			play.hurt();
		}
		return true;
	}
}

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[2], 0, 0, obj.var[0], 1, 1, SPRITE::SINGLEHUE, 32);
		}
	}
}

class DragonShockwave : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::STEADYLIGHT, false);
		obj.xPos = obj.xOrg + 24;
		obj.lightType = obj.var[0] == 1? LIGHT::RING2 : LIGHT::RING;
		obj.var[1] = obj.light * 4;
		obj.counter++;
		if (obj.counter == 1) {
			obj.light = 100;
			jjSamplePriority(SOUND::ORANGE_SWEEP0L);
		}
		if (obj.counter > 150) obj.delete();
		if (obj.var[0] == 0) {
			if (obj.light > 0) {
				obj.light--;
				charging = true;
			} else {
				obj.var[0] = 1;
				for (int i = 0; i < 5; i++) {
					jjSamplePriority(SOUND::ORANGE_BOEMR);
				}
				charging = false;
			}
		} else {
			obj.light += 3;
		}
		
		for (int i = 0; i < jjLocalPlayerCount; i++) {
			jjPLAYER@ play = jjLocalPlayers[i];
			float dx = play.xPos - obj.xPos, dy = play.yPos - obj.yPos;
			if (dx * dx + dy * dy < obj.var[1] * obj.var[1]) {
				if (obj.var[8] & 1 << i == 0 && obj.var[0] == 1) {
					play.hurt(2, play.blink == 0 && play.buttstomp < 41? true:false);
					obj.var[8] = obj.var[8] | 1 << i;
				}
			}
		}
		for (int j = 1; j < jjObjectCount; j++) {
			jjOBJ@ bull = jjObjects[j];
			float dx = bull.xPos - obj.xPos, dy = bull.yPos - obj.yPos;
			if (dx * dx + dy * dy < obj.var[1] * obj.var[1]) {
				if (bull.playerHandling == HANDLING::PLAYERBULLET && bull.var[9] == 0 && obj.var[0] == 1) {
					bull.ricochet();
				}
			}
		}
	}
}

const array<dragonStates> closeRangeAttacks = {Idle, Roar, Roar, Homing_Fire, Homing_Fire, Spawn_FireOrb, Shockwave};
const array<dragonStates> longRangeAttacks = {Idle, Idle, Roar, Homing_Fire, Flame_Burst, Flame_Burst, Flamethrower, Firebombs};

array<dragonStates> closeRangeQueue;
array<dragonStates> longRangeQueue;

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

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

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

void randomAttack(jjOBJ@ obj) {
    int playerID = obj.findNearestPlayer(100000);
    if (playerID > -1)
        currState = getNextAttack(closeRangeQueue, closeRangeAttacks);
    else
        currState = getNextAttack(longRangeQueue, longRangeAttacks);
    obj.special = 0;
    obj.counter = 0;
}

void dragonShout(jjOBJ@ obj, int rate) {
	uint random = jjRandom();
	int magnitude = (2 << (random & 3)) - 1;
	int halfMagnitude = magnitude >> 1;
				
	if (jjGameTicks & 1 == 0)
		jjLocalPlayers[0].cameraFreeze(jjLocalPlayers[0].cameraX + (random >> 8 & magnitude) - halfMagnitude, jjLocalPlayers[0].cameraY + (random >> 2 & magnitude) - halfMagnitude, false, true);
	else
		jjLocalPlayers[0].cameraUnfreeze();
						
	int fireRate = rate;
	if (obj.special % fireRate == 0) {
		if (jjRandom()%randcar < (randcar - 1)) {
			Icicle temp;
			jjOBJ@ icicle = jjObjects[jjAddObject(OBJECT::BLASTERBULLET, (int(obj.xPos - 890) + jjRandom()%830), int(obj.yPos - 580), 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;
			icicle.lightType = LIGHT::NONE;
		} else {
			jjOBJ@ carrot = jjObjects[jjAddObject(OBJECT::CARROT, (int(obj.xPos - 890) + jjRandom()%830), int(obj.yPos - 580), obj.objectID, CREATOR::OBJECT)];
			carrot.state = STATE::FLOATFALL;
		}
	}
}

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;
}

void onPlayer(jjPLAYER@ play) {
	if (jjGameTicks % 350 == 0 && !dragonActivated) jjSample(play.xPos + 128, play.yPos, SOUND::INTRO_MONSTER, play.xPos > 67*32? 32:16, 0);
}

void onMain() {
	for (int i = 1; i < jjObjectCount; i++) {
		if (jjObjects[i].eventID == OBJECT::BOMBCRATE) {
			jjObjects[i].yPos = jjObjects[i].yOrg;
		}
	}
	if (defeated) elapsed++;
	if (elapsed == 350) jjNxt("HH17_Ending.j2l", false, false);
}
void onFunction0(jjPLAYER@ play) {
	play.activateBoss(true);
	if (!dragonActivated) {
		dragonActivated = true;
		play.showText("@@@@@@@A big, bad dragon blocks your path!", STRING::MEDIUM);
	}
}