Downloads containing JungUltEx1.j2as

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Jungle UltimateFeatured Download DoubleGJ Tileset conversion 10 Download file

File preview

const bool MLLESetupSuccessful = MLLE::Setup(); ///@MLLE-Generated
#include "MLLE-Include-1.7.asc" ///@MLLE-Generated
#pragma require "JungUltEx1-MLLE-Data-2.j2l" ///@MLLE-Generated
#pragma require "JungUltEx1-MLLE-Data-1.j2l" ///@MLLE-Generated
#pragma require "JungUltEx1.j2l" ///@MLLE-Generated
#pragma require "DD-Anims.j2a"
#include "DD-Order.asc"
#include "DD-Rank.asc"
#pragma require "JungUlt-Canopy1.png"
#pragma require "JungUlt-Canopy2.png"
#pragma require "JungUlt-Foreground1.png"
#pragma require "JungUlt-Foreground2.png"
#pragma require "JungUlt-Foreground3.png"
#pragma require "JungUlt-Foreground4.png"
#pragma require "JungUlt-Foreground5.png"
#pragma require "waterfall-loop.wav"
#pragma require "big-bite.wav"
#pragma require "common-gemsmsh1.wav"
#pragma require "acid2.wav"
#pragma require "aslime2.wav"
#pragma require "empty.wav" // endall solution for unwanted sounds
#pragma require "kookaburra-loop1.wav"
#pragma require "kookaburra-loop2.wav"
#pragma require "gibbon-calls.wav"
#pragma require "frog-croak.wav"
#pragma require "jungle-calls.wav"
#pragma require "jungle-ambience.wav"
#pragma require "jungle-birdloop.wav"
#pragma require "frog-loop.wav"
#pragma require "jungle-birds.wav"
#pragma require "bird-takeoff1.wav"
#pragma require "bird-takeoff2.wav"
#pragma require "bird-takeoff3.wav"
#pragma require "nakotak1.wav"
#pragma require "nakotak2.wav"
#pragma require "bonk.wav"
#pragma require "vonyipp-death1.wav"
#pragma require "laser.wav" // I dream of the day when I can input SOUND::sound instead of a file in jjSampleLoad...
#pragma require "phaser2.wav"
#pragma require "rustle.wav"
#pragma require "bigcopter.wav"
#pragma offer "DD-Jungle.it"
#pragma offer "CD-Normal.mod"
#pragma offer "firage_-_buns_and_guns_2.it"
#pragma offer "damn-akane.mp3"
#pragma offer "bloopin.mp3"

const int bestTime = 570;
const float maxScore = 120000;
const int easyTimer = 0;
const int normalTimer = 0;
const int hardTimer = 84000;
const int turboTimer = 63000;

// Devan stuff:
array<jjLAYER@>@ backupLayerOrder = array<jjLAYER@> = {jjLayers[1], jjLayers[2], jjLayers[3], jjLayers[4], jjLayers[5], jjLayers[6], jjLayers[7], jjLayers[8]};
array<jjLAYER@>@ bossLayerOrder = array<jjLAYER@> = {jjLayers[1], jjLayers[2], jjLayers[3], jjLayers[4], jjLayers[5], jjLayers[6], jjLayers[7], jjLayers[8]};
bool boemFloor;
bool devanWait = false;
bool foughtDevan = false;
bool endFlag = false;
bool layer2Destroyed = false;
CustomBossHealthBar customBossHealthBar;
int bossHealth, bossPhase;

// Jill stuff:
bool jillNear = false;
bool drawBirdie = true;
bool jillMet = false;
bool beenBirdie = false;
int farPlayerID = 0;
int nearPlayerID = 0;

bool copterSpawned, copterDefeated = false;
int startHP = 5;
int copterChannel, emoteFade = 0;
const uint NumberOfBitsDevotedToSyncParameter = 3; //this should correspond to the number of bits you assign to the Sync parameter in your JCS.ini (or MLLE equivalent) entry for Swinging Vine. So for example if you give it a parameter Sync:2, this variable should ALSO equal 2.

void onLevelLoad() {
	initiateDialogue();
	initiateRanking();
	initFade();

	jjAnimSets[ANIM::HELMUT].load(); // for pixelmaps	
	jjAnimSets[ANIM::BUBBA].load(); // for ambient sfx
	jjAnimSets[ANIM::CHUCK].load(); // birdie friend!
	jjSampleLoad(SOUND::SCIENCE_PLOPKAOS, "waterfall-loop.wav");
	jjSampleLoad(SOUND::BUBBA_BUBBABOUNCE1, "kookaburra-loop1.wav");
	jjSampleLoad(SOUND::BUBBA_BUBBABOUNCE2, "kookaburra-loop2.wav");
	jjSampleLoad(SOUND::BUBBA_BUBBAEXPLO, "gibbon-calls.wav");
	jjSampleLoad(SOUND::BUBBA_FROG2, "frog-croak.wav");
	jjSampleLoad(SOUND::BUBBA_FROG3, "jungle-calls.wav");
	jjSampleLoad(SOUND::BUBBA_FROG4, "jungle-ambience.wav");
	jjSampleLoad(SOUND::BUBBA_FROG5, "jungle-birdloop.wav");
	jjSampleLoad(SOUND::BUBBA_SNEEZE2, "frog-loop.wav");
	jjSampleLoad(SOUND::BUBBA_TORNADOATTACK2, "jungle-birds.wav");
	
	jjAnimSets[ANIM::DEVILDEVAN].load();
	jjAnimSets[ANIM::DEVAN].load();
	jjAnimSets[ANIM::BILSBOSS].load();
	jjAnimSets[ANIM::CUSTOM[19]].load(19, "DD-Anims.j2a"); // Devan extra sprites
	tweakDevanRemoteAnim();
	DDevan(jjObjectPresets[OBJECT::DEVILDEVAN]);
	jjSampleLoad(SOUND::INTRO_MONSTER2, "vonyipp-death1.wav");

	jjAnimSets[ANIM::CUSTOM[7]].load(3, "DD-Anims.j2a"); // custom pickups sprites
///@Event 67=Super Item |+|Goodies |Super |Item |Type:{Red Gem,Green Gem,Blue Gem,Purple Gem,Watermelon,Jackfruit}3
	jjObjectPresets[OBJECT::SUPERGEM].behavior = SuperGemWrapper;
	jjObjectPresets[OBJECT::FLICKERGEM].behavior = flickerWrapper;
	jjSampleLoad(SOUND::COMMON_GEMSMSH1, "empty.wav");
	jjAnimSets[ANIM::DOG].load();
	jjSampleLoad(SOUND::DOG_SNIF1, "common-gemsmsh1.wav");
///@Event 174=Kiwi |+|Food |Kiwi
	jjObjectPresets[OBJECT::BURGER].determineCurAnim(ANIM::CUSTOM[7], 1);
///@Event 175=Pineapple |+|Food | Pine |Apple	
	jjObjectPresets[OBJECT::PIZZA].determineCurAnim(ANIM::CUSTOM[7], 13);
///@Event 168=Mango |+|Food |Mango
	jjObjectPresets[OBJECT::DONUT].determineCurAnim(ANIM::CUSTOM[7], 14);
// not renaming Purple Gem since it's just a reskin
	jjObjectPresets[OBJECT::PURPLEGEM].behavior = specialApple();
	jjObjectPresets[OBJECT::PURPLEGEM].determineCurAnim(ANIM::CUSTOM[7], 16);
///@Event 176=Melon |+|Food |Melon
	jjObjectPresets[OBJECT::FRIES].determineCurAnim(ANIM::CUSTOM[7], 15);
///@Event 180=Avocado |+|Food |Avo |Cado
	jjObjectPresets[OBJECT::WEENIE].determineCurAnim(ANIM::CUSTOM[7], 17);
///@Event 181=Dragon Fruit |+|Food |Dragon |Fruit
	jjObjectPresets[OBJECT::HAM].determineCurAnim(ANIM::CUSTOM[7], 18);
///@Event 182=Pomegranate |+|Food |Pome |Granat
	jjObjectPresets[OBJECT::CHEESE].determineCurAnim(ANIM::CUSTOM[7], 19);
///@Event 167=Lychee |+|Food |Lychee
	jjObjectPresets[OBJECT::CAKE].determineCurAnim(ANIM::CUSTOM[7], 20);
///@Event 169=Star Fruit |+|Food |Star |Fruit
	jjObjectPresets[OBJECT::CUPCAKE].determineCurAnim(ANIM::CUSTOM[7], 21);
///@Event 170=White Grapes |+|Food |White |Grapes
	jjObjectPresets[OBJECT::CHIPS].determineCurAnim(ANIM::CUSTOM[7], 11);
///@Event 141=Persimmon |+|Food |Persi-|mmon
	jjObjectPresets[OBJECT::APPLE].determineCurAnim(ANIM::CUSTOM[7], 35);
///@Event 177=Nectarine |+|Food |Necta-|rine
	jjObjectPresets[OBJECT::CHICKENLEG].determineCurAnim(ANIM::CUSTOM[7], 36);
///@Event 172=Ginger Root|+|Food |Ginger
	jjObjectPresets[OBJECT::CHOCBAR].determineCurAnim(ANIM::CUSTOM[7], 65);
///@Event 162=Lulo |+|Food |Lulo
	jjObjectPresets[OBJECT::CUCUMBER].determineCurAnim(ANIM::CUSTOM[7], 26);
///@Event 146=Papaya |+|Food |Papaya
	jjObjectPresets[OBJECT::PRETZEL].determineCurAnim(ANIM::CUSTOM[7], 29);
///@Event 161=Guanabana |+|Food |Guana-|bana
	jjObjectPresets[OBJECT::EGGPLANT].determineCurAnim(ANIM::CUSTOM[7], 53);
///@Event 178=Cherimoya |+|Food |Che-|rimoya
	jjObjectPresets[OBJECT::SANDWICH].determineCurAnim(ANIM::CUSTOM[7], 46);

///@Event 166=Parsley Max Energy +1 |+|Goodies |Parsley	
	jjObjectPresets[OBJECT::PIE].determineCurAnim(ANIM::CUSTOM[7], 2);
	jjObjectPresets[OBJECT::PIE].scriptedCollisions = true;
	jjObjectPresets[OBJECT::PIE].behavior = parsley();
	startHP = jjMaxHealth;

///@Event 111=Lori Block |+|Trigger |Lori |Scen	
	jjOBJ@ myDestructSceneryVariant = jjObjectPresets[OBJECT::CHESHIRE1];
	myDestructSceneryVariant.points = 50;
    myDestructSceneryVariant.playerHandling = HANDLING::SELFCOLLISION;
    myDestructSceneryVariant.behavior = loriBlock;

///@Event 179=Big Grub |+|Object |Big |Grub
	jjAnimSets[ANIM::CUSTOM[2]].load(15, "DD-Anims.j2a"); // dragonfly extra sprites
	jjSampleLoad(SOUND::DOG_AGRESSIV, "big-bite.wav");
	CustomDragonfly(jjObjectPresets[OBJECT::DRAGONFLY]);
	jjObjectPresets[OBJECT::TACO].determineCurAnim(ANIM::CUSTOM[2], 0);
	jjObjectPresets[OBJECT::TACO].behavior = bigGrub();
///@Event 196=Grub |-|Enemy |Grub | |Stick to:{Ground,Right wall,Ceiling,Left wall}2	
	jjOBJ@ grubPreset = jjObjectPresets[OBJECT::CRAB];
	grubPreset.behavior = grub();
	grubPreset.determineCurAnim(ANIM::CUSTOM[2], 2);
	grubPreset.energy = 1;
	grubPreset.points = 100;

///@Event 42=Swinging Vine |+|Object |Swing |Vine |Sync:3 |Length:4	
	jjObjectPresets[OBJECT::SWINGINGVINE].behavior = SyncedVine;

///@Event 217=Jill of the Jungle |+|Morph |Jill
	jjAnimSets[ANIM::CUSTOM[3]].load(16, "DD-Anims.j2a");	// Jill sprite
	jjAnimSets[ANIM::CUSTOM[4]].load(5, "DD-Anims.j2a"); // dialogue prompt sprite
	jjObjectPresets[OBJECT::EVA].behavior = jill();
	jjObjectPresets[OBJECT::EVA].determineCurAnim(ANIM::CUSTOM[3], 0);
	jjObjectPresets[OBJECT::EVA].bulletHandling = HANDLING::IGNOREBULLET;
	jjSampleLoad(SOUND::COMMON_SHIELD_ELEC, "empty.wav"); // yeah you won't hear the plasma shield if you use a cheat in this level, sorry

///@Event 100=Shaman Demon |-|Enemy |Shaman |Demon
	jjAnimSets[ANIM::CUSTOM[9]].load(20, "DD-Anims.j2a"); // shaman demon sprites
	jjSampleLoad(SOUND::INTRO_INHALE, "nakotak1.wav");
	jjSampleLoad(SOUND::INTRO_UHTURT, "nakotak2.wav");
	jjSampleLoad(SOUND::INTRO_HITTURT, "bonk.wav");
	jjOBJ@ shamanDemonPreset = jjObjectPresets[OBJECT::TUFTURT];
	shamanDemonPreset.behavior = shamanDemon();
	shamanDemonPreset.energy = 15 + (jjDifficulty * 5) ;
	shamanDemonPreset.points = 1000;
	shamanDemonPreset.scriptedCollisions = true;
	shamanDemonPreset.playerHandling = HANDLING::SPECIAL;
	
///@Event 106=
	jjOBJ@ stormCloudPreset = jjObjectPresets[OBJECT::RAPIER];
	stormCloudPreset.behavior = stormCloud();
	stormCloudPreset.determineCurAnim(ANIM::CUSTOM[9], 4);
	stormCloudPreset.playerHandling = HANDLING::PARTICLE;
	stormCloudPreset.bulletHandling = HANDLING::IGNOREBULLET;
	stormCloudPreset.isTarget = false;
	stormCloudPreset.isFreezable = false;
	stormCloudPreset.triggersTNT = false;
	stormCloudPreset.deactivates = true;

///@Event 237=Lizard (Heavy Copter) |-|Enemy |Heavy |Fl.Liz
	jjSampleLoad(SOUND::ORANGE_BUBBELSR, "bigcopter.wav");
	jjAnimSets[ANIM::CUSTOM[23]].load(23, "DD-Anims.j2a");
	jjOBJ@ preset = jjObjectPresets[OBJECT::BEEBOY];
	preset.determineCurAnim(ANIM::CUSTOM[23], 3);
	preset.frameID = 0;
	preset.behavior = HeavyCopter();
	preset.energy = 35;
	preset.points = 1000;
	preset.xSpeed = preset.ySpeed = 0;
	preset.scriptedCollisions = true;
	preset.playerHandling = HANDLING::SPECIAL;
	preset.bulletHandling = HANDLING::DETECTBULLET;
	preset.causesRicochet = false;
	preset.isTarget = true;
	preset.isBlastable = true;
	preset.isFreezable = true;
	preset.triggersTNT = true;
	preset.state = STATE::START;
	preset.lightType = LIGHT::NONE;
	preset.direction = 1;
	preset.var[hcNextPhysicalInjury] = 0;
	preset.var[hcNextNearbyPlayerCheck] = 0;
	preset.var[hcTargetPlayerID] = -1;
	preset.var[hcCurrentAngle] = 0; //0 = forwards, 17 = down, 1-16 = turning (each frame is 4 ticks)
	
	jjAnimSets[ANIM::CUSTOM[5]].load(17, "DD-Anims.j2a"); // emote
	jjSampleLoad(SOUND::DOG_WAF1, "acid2.wav");
	
///@Event 204=Falling Log (single) |+|Object |Log
	jjAnimSets[ANIM::CUSTOM[6]].load(10, "DD-Anims.j2a"); // log sprite
	Log(jjObjectPresets[OBJECT::PSYCHPOLE]);
///@Event 79=Log Spawner |+|Object |Log |Start|Amount:4 |Delay:4
	LogSpawner(jjObjectPresets[OBJECT::FASTFEET]);
///@Event 8=Log Resetter |+|Object |Log |Reset

///@Event 126=Lava Block |+|Scenery |Lava |Block | Adjust Y:5
	jjAnimSets[ANIM::CUSTOM[12]].load(9, "DD-Anims.j2a"); // visual fx
	jjSampleLoad(SOUND::WIND_WIND2A, "aslime2.wav");
	jjOBJ@ lavaBlockPreset = jjObjectPresets[OBJECT::FENCER]; // not expecting anyone to put fencers in a level with lava in it
	lavaBlockPreset.behavior = lavaBlock();
	lavaBlockPreset.determineCurAnim(ANIM::DESTSCEN, 4);
	lavaBlockPreset.playerHandling = HANDLING::SPECIAL;
	lavaBlockPreset.scriptedCollisions = true;
	lavaBlockPreset.bulletHandling = HANDLING::IGNOREBULLET;
	lavaBlockPreset.isTarget = false;
	lavaBlockPreset.isFreezable = false;
	lavaBlockPreset.triggersTNT = false;
	lavaBlockPreset.deactivates = true;
	lavaBlockPreset.energy = 50; // just in case

///@Event 205=Fireball Turret  |+|Object    |Fire |Turret  |Adjust Y:5|Adjust X:-6 |Direction:{Left,Right}1 |Speed:3
	jjObjectPresets[OBJECT::DIAMONDUSPOLE].behavior = totemTurret();
	jjObjectPresets[OBJECT::DIAMONDUSPOLE].determineCurAnim(ANIM::CUSTOM[12], 0);

///@Event 127=Sparkle |+|Scenery    |Sparkle
	jjObjectPresets[OBJECT::FISH].behavior = sparkle;
	jjObjectPresets[OBJECT::FISH].determineCurAnim(ANIM::CUSTOM[12], 0);
	jjObjectPresets[OBJECT::FISH].playerHandling = HANDLING::PARTICLE;
	jjObjectPresets[OBJECT::FISH].bulletHandling = HANDLING::IGNOREBULLET;
	jjObjectPresets[OBJECT::FISH].triggersTNT = false;
	jjObjectPresets[OBJECT::FISH].isTarget = false;
	jjObjectPresets[OBJECT::FISH].isFreezable = false;

///@Event 191=Little Bird |+|Scenery |Bird |Bird	
	jjSampleLoad(SOUND::INTRO_UP1, "bird-takeoff1.wav");
	jjSampleLoad(SOUND::INTRO_UP2, "bird-takeoff2.wav");
	jjSampleLoad(SOUND::INTRO_SKI, "bird-takeoff3.wav");
	jjAnimSets[ANIM::CUSTOM[25]].load(25, "DD-Anims.j2a");
	jjOBJ@ presetObject = jjObjectPresets[OBJECT::TUBETURTLE];
	presetObject.behavior = littleBird();
	presetObject.playerHandling = HANDLING::PARTICLE;
	presetObject.bulletHandling = HANDLING::IGNOREBULLET;
	presetObject.isTarget = false;
	presetObject.isFreezable = false;
	presetObject.triggersTNT = false;
	presetObject.deactivates = true;

///@Event 113=Guerrilla Doofus |-|Enemy |Guerla |Doofus
	jjAnimSets[ANIM::PLUS_SCENERY].load();
	jjAnimSets[ANIM::CUSTOM[22]].load(22, "DD-Anims.j2a");
	jjSampleLoad(SOUND::INTRO_GREN2, "rustle.wav");
	jjOBJ@ guerrillaPreset = jjObjectPresets[OBJECT::HATTER];
	guerrillaPreset.behavior = guerrillaDoofus();
	guerrillaPreset.energy = 5;
	guerrillaPreset.points = 500;

///@Event 254=Delet This |-|Modifier |NOT |v
    deleteUnwantedEvents();

// custom platform pixelmap:
	jjPIXELMAP platform((17 * 32), 0, 96, 32, 4);
	jjANIMFRAME@ grassplat = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::GRASSPLAT].firstAnim].firstFrame];
	platform.save(grassplat);
	grassplat.hotSpotX = -grassplat.width/2;
	grassplat.hotSpotY = -grassplat.height/2 + 10;
	
// flip Carrotus Pole to match other trees
	jjPIXELMAP carrpoleimg(jjAnimFrames[jjAnimations[jjAnimSets[ANIM::CARROTPOLE].firstAnim].firstFrame]);
	jjANIMFRAME@ carrpole = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::CARROTPOLE].firstAnim].firstFrame];
	carrpoleimg.flip(SPRITE::FLIPH);
	carrpoleimg.save(carrpole);
	carrpole.hotSpotX = -10;

// canopy layer 1 pixelmap:
	jjPIXELMAP tree1("JungUlt-Canopy1.png");
	tree1.crop(0, 64, 960, 352); // shaving off top two tiles (you might not need to)
	jjANIMFRAME@ helmut1 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim].firstFrame];
	tree1.save(helmut1);
	helmut1.hotSpotX = 0;
	helmut1.hotSpotY = 0;
	
// canopy layer 2 pixelmap:
	jjPIXELMAP tree2("JungUlt-Canopy2.png");
	jjANIMFRAME@ helmut2 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim].firstFrame + 1];
	tree2.save(helmut2);
	helmut2.hotSpotX = 0;
	helmut2.hotSpotY = 0;
	
// foreground piece 1 pixelmap:
	jjPIXELMAP fore1("JungUlt-Foreground1.png");
	jjANIMFRAME@ helmut3 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim].firstFrame + 2];
	fore1.save(helmut3);
	helmut3.hotSpotX = 0;
	helmut3.hotSpotY = 0;
	
// foreground piece 2 pixelmap:
	jjPIXELMAP fore2("JungUlt-Foreground2.png");
	jjANIMFRAME@ helmut4 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim + 1].firstFrame];
	fore2.save(helmut4);
	helmut4.hotSpotX = 0;
	helmut4.hotSpotY = 0;
	
// foreground piece 3 pixelmap:
	jjPIXELMAP fore3("JungUlt-Foreground3.png");
	jjANIMFRAME@ helmut5 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim + 1].firstFrame + 1];
	fore3.save(helmut5);
	helmut5.hotSpotX = 0;
	helmut5.hotSpotY = 0;
	
// foreground piece 4 pixelmap:
	jjPIXELMAP fore4("JungUlt-Foreground4.png");
	jjANIMFRAME@ helmut6 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim + 1].firstFrame + 2];
	fore4.save(helmut6);
	helmut6.hotSpotX = 0;
	helmut6.hotSpotY = 0;
	
// foreground piece 5 pixelmap:
	jjPIXELMAP fore5("JungUlt-Foreground5.png");
	jjANIMFRAME@ helmut7 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim + 1].firstFrame + 3];
	fore5.save(helmut7);
	helmut7.hotSpotX = 0;
	helmut7.hotSpotY = 0;
	
	jjLAYER@ water = MLLE::GetLayer("Water");
	water.spriteMode = SPRITE::TRANSLUCENT;
	
// canopy sunrays layer:
	jjLAYER@ sunrays = MLLE::GetLayer("Sunrays");
	sunrays.spriteParam = 192;
	sunrays.spriteMode = SPRITE::BLEND_LIGHTEN;
	sunrays.textureSurface = SURFACE::FULLSCREEN;
	sunrays.textureStyle = TEXTURE::WARPHORIZON;
	sunrays.warpHorizon.setFadeColor(jjPALCOLOR(7,219,223));
	sunrays.xSpeed = sunrays.ySpeed = 0.05;
	sunrays.warpHorizon.fadePositionX = 0.7;
	sunrays.warpHorizon.fadePositionY = 0;
	
	jjLAYER@ sky = jjLayers[8];
	sky.texture = TEXTURE::NORMAL; // this version of the tileset doesn't include the original sky texture cuz you can simply do this
	sky.xSpeed = 0.02;
	sky.ySpeed = 0.02;
	sky.xAutoSpeed = -0.04;
	sky.xSpeedModel = LAYERSPEEDMODEL::NORMAL;
	sky.ySpeedModel = LAYERSPEEDMODEL::NORMAL;
	
// final layer setup for boss
	backupLayerOrder = jjLayerOrderGet();
	bossLayerOrder = backupLayerOrder;
	bossLayerOrder.removeRange(1,3);
}

void onLevelBegin() {
	MLLE::SpawnOffgrids();
	setTimer();
}

void onLevelReload() {
	MLLE::SpawnOffgridsLocal();
	copterSpawned = false;
	copterDefeated = false;
	beenBirdie = false;
	DDevan(jjObjectPresets[OBJECT::DEVILDEVAN]);
	boemFloor = false;
	customBossHealthBar.ready = false;
	scoreSeconds = storeScoreSeconds;
	if (foughtDevan) {
		jjMusicLoad("DD-Jungle.it");
		jjSampleLoad(SOUND::DEVILDEVAN_PHASER2, "phaser2.wav"); // revert ghetto solution
	}
	jjChat("/smhealth " + startHP);		
	for (int i = 0; i < 8; ++i) jjAlert("");
}

void onMain() {
	if (jjDifficulty == 0) oneUpsIntoBlueGems();
	if ((jjGameTicks % 70) == 0 && !levelOver && CurrentPopup is null) scoreSeconds++;	
	rankingSetup();
	if (levelOver) updateFade();
	
	int boem;
	
	if ((jjGameTicks % 5) == 0 && boemFloor == true) {
		boem = jjAddObject(OBJECT::RFBULLET, (474 * 32) + (jjRandom()%256 * 4), (26 * 32) + (jjRandom()%24 * 4));
		jjObjects[boem].state = STATE::EXPLODE;
	}
	if (jjTriggers[1] && !layer2Destroyed) {
		jjLayerOrderSet(bossLayerOrder);
		layer2Destroyed = true;
	}
	else if (!jjTriggers[1] && layer2Destroyed) {
		jjLayerOrderSet(backupLayerOrder);
		layer2Destroyed = false;
	}
	
	
	if (jjTriggers[2] == true && emoteFade < 255) { 
	emoteFade++;
		if (emoteFade == 1) {
			jjSamplePriority(SOUND::DOG_WAF1);
			jjLocalPlayers[1].showText("@@@@#MY MAN", STRING::LARGE); // to do someday: make co-op friendly
		}
	}
	else if (jjTriggers[2] == false) emoteFade = 0;
	if (copterDefeated) {
		for (int i = 1; i < jjObjectCount; i++) {
			jjOBJ@ scen = jjObjects[i];
			if (scen.eventID == OBJECT::DESTRUCTSCENERYBOMB && scen.state == STATE::SLEEP) {
				scen.state = STATE::KILL;
//				copterDefeated = false;
			}
		}
	}
}

void onPlayer(jjPLAYER@ play) {
	dialogueInterface(play);
	rankingInterface(play);

	if (play.bossActivated) {
		for (int i = 1; i < jjObjectCount; i++) {
		jjOBJ@ obj = jjObjects[i];
		if (obj.eventID == OBJECT::DEVILDEVAN) {
			if (obj.state == STATE::DONE) {
				play.keyLeft = play.keyRight = play.keyDown = play.keyUp = play.keyRun = play.keyFire = play.keyJump = play.keySelect = false;
			}
			if (obj.state == STATE::WALK && devanWait) {
				obj.state = STATE::STILL;
				obj.determineCurAnim(ANIM::DEVILDEVAN, 8);
				}
			if (uint(obj.curAnim) == jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 6) {
				boemFloor = true;
				jjMusicStop();
			}
			if (uint(obj.curAnim) == jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 5) {
				jjMusicLoad("damn-akane.mp3");
				if (!jjTriggers[1]) {
					for (int a = 0; a < 8; a++) {
						jjOBJ@ lavaBlub = jjObjects[jjAddObject(OBJECT::AMBIENTSOUND, (476 * 32) + (a * 7 * 32) + 16, (28 * 32) + 16)];
						lavaBlub.var[1] = 96;
					}
				}
				jjTriggers[1] = true;
				boemFloor = false;
				}
			if (uint(obj.curAnim) == jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 1) {
				if (play.bossActivated == true) { endFlag = true; }
				play.activateBoss(false);
				@CurrentPopup = Conversation(array<Screen@> = {
				Screen(
					top: Line("okay buster, now TALK! where's bubba?!", left: Jazz, direction: -1)
					),
				Screen(
					top: Line(left: Jazz),
					bottom: Line("h-he's on that hill behind you, doing a RITUAL of some sort! now leave me ALONE!", right: Devan, direction: 1, color: 80)
					),
				Screen(
					top: Line("yeah, sure, like i'll FALL for that.", left: Jazz, direction: -1),
					bottom: Line(right: Devan, direction: 1, color: 80)
					),
				Screen(
					top: Line("jazz, wait. that does explain why they came to this jungle in the first place. given, you know... its TEMPORALLY DISPLACED aura.", left: Jazz, right: Lori, direction: 1, color: 40),
					bottom: Line("what she said. but that's really ALL i know!", right: Devan, direction: 1, color: 80)
					),
				Screen(
					top: Line("so... that HILL over there? behind the VILLAGE?", left: Jazz, right: Lori, direction: -1),
					bottom: Line("yes, YES!!", right: Devan, direction: 1, color: 80)
					),
				Screen(
					top: Line(left: Jazz, right: Lori),
					bottom: Line("well, break a leg or WHATEVER! my personal translocator is all charged up now, so i'm OUTTA HERE!", right: Devan, direction: 1, color: 80),
					finish: function(bool skipping) {
						jjLocalPlayers[0].boss = 0;
						jjSamplePriority(SOUND::INTRO_MONSTER2);
						endFlag = false;
						endLevel(jjLocalPlayers[0]);
					}
					),
				});
				}
			}
		}
	}
	
	if (play.charCurr == CHAR::BIRD) {
	if (play.shieldTime == 0) {
		endBirdie(play);
		}
	else { 	jjSetModTempo(uint8(-1.75 * int(play.shieldTime / 70) + 210)); }
	}
}

void onPlayerDraw(jjPLAYERDRAW& draw) {
	jjPLAYER@ play = draw.player;
	if (play.charCurr == CHAR::BIRD) {
		draw.shield[SHIELD::PLASMA] = false;
		return;
	}
}

bool onDrawScore(jjPLAYER@ player, jjCANVAS@ canvas) {
	if (jillNear) {
		canvas.drawSprite((jjSubscreenWidth / 2) - 16, jjSubscreenHeight - (jjSubscreenHeight / 32), ANIM::CUSTOM[7], 16, 0);
		canvas.drawString(jjSubscreenWidth / 2, jjSubscreenHeight - (jjSubscreenHeight / 32), "x" + player.gems[GEM::PURPLE], STRING::MEDIUM);
	}
	if (CurrentPopup !is null) {
		CurrentPopup.Draw(canvas);
		return !CurrentPopup.DrawHUD();
	}
	if (levelOver) {
		drawFade(player, canvas);
		if (rankLine < 6) rankingDisplay(player, canvas);
	}
	if (rankLine >= 6) return true;
	else return false;
}

bool onDrawAmmo(jjPLAYER@ player, jjCANVAS@ canvas) {
	if (jjColorDepth < 16) {
		canvas.drawRectangle(0, 0, jjSubscreenWidth, jjSubscreenHeight, 79);
		canvas.drawString((jjSubscreenWidth / 16), (jjSubscreenHeight / 2) - (jjSubscreenHeight / 8), "This level does not", smallScreen ? STRING::SMALL : STRING::MEDIUM);
		canvas.drawString((jjSubscreenWidth / 16), (jjSubscreenHeight / 2) - (jjSubscreenHeight / 16), "support 8-bit color depth.", smallScreen ? STRING::SMALL : STRING::MEDIUM);
		canvas.drawString((jjSubscreenWidth / 16), (jjSubscreenHeight / 2) + (jjSubscreenHeight / 16), "Please go to options menu", smallScreen ? STRING::SMALL : STRING::MEDIUM);
		canvas.drawString((jjSubscreenWidth / 16), (jjSubscreenHeight / 2) + (jjSubscreenHeight / 8), "and switch to 16-bit color.", smallScreen ? STRING::SMALL : STRING::MEDIUM);
	}
	if (rankLine >= 6) return true;
	else return CurrentPopup !is null && !CurrentPopup.DrawHUD();
}

bool onDrawHealth(jjPLAYER@ player, jjCANVAS@ canvas) {
	if (rankLine >= 6) return true;
	else return CurrentPopup !is null && !CurrentPopup.DrawHUD();
}
bool onDrawLives(jjPLAYER@ player, jjCANVAS@ canvas) { 
    if (customBossHealthBar.ready) customBossHealthBar.draw(canvas);
	if (jjDifficulty == 0 && CurrentPopup is null) {
		return true;
	}
	if (rankLine >= 6) return true;
    else return CurrentPopup !is null && !CurrentPopup.DrawHUD();
}
bool onDrawPlayerTimer(jjPLAYER@ player, jjCANVAS@ canvas) { return CurrentPopup !is null && !CurrentPopup.DrawHUD(); }

// putting foreground eyecandy on layer 1:
void onDrawLayer1(jjPLAYER@ player, jjCANVAS@ canvas) {
	canvas.drawSprite(0, (14 * 32), ANIM::HELMUT, 0, 2);
	canvas.drawSprite(269, (34 * 32), ANIM::HELMUT, 1, 0, -1);
	canvas.drawSprite(0, (94 * 32), ANIM::HELMUT, 0, 2);
	canvas.drawSprite((416 * 32), (38 * 32), ANIM::HELMUT, 0, 2);
	canvas.drawSprite((415 * 32), (11 * 32), ANIM::HELMUT, 0, 2, -1);
	canvas.drawSprite((100 * 32), 0, ANIM::HELMUT, 1, 1);
	canvas.drawSprite((200 * 32), 0, ANIM::HELMUT, 1, 1, -1);
	canvas.drawSprite((400 * 32), 0, ANIM::HELMUT, 1, 1, -1);
	canvas.drawSprite((124 * 32), (109 * 32), ANIM::HELMUT, 1, 2);
	canvas.drawSprite((520 * 32), (102 * 32), ANIM::HELMUT, 1, 2);
	canvas.drawSprite((604 * 32), (105 * 32), ANIM::HELMUT, 1, 2, -1);
	canvas.drawSprite((294 * 32), 0, ANIM::HELMUT, 1, 1);
	canvas.drawSprite((250 * 32), 0, ANIM::HELMUT, 1, 3);
	canvas.drawSprite((340 * 32), 0, ANIM::HELMUT, 1, 3, -1);
	canvas.drawSprite((550 * 32), 0, ANIM::HELMUT, 1, 3);
	canvas.drawSprite((500 * 32), 0, ANIM::HELMUT, 1, 1);
	canvas.drawSprite((610 * 32), 0, ANIM::HELMUT, 0, 2, SPRITE::FLIPV);
	canvas.drawSprite((747 * 32) + 20, (97 * 32), ANIM::HELMUT, 1, 0);
	if (player.charCurr == CHAR::BIRD) canvas.drawSprite((746 * 32), (47 * 32) - 10, ANIM::HELMUT, 1, 1, -1);
}

void onDrawLayer4(jjPLAYER@ player, jjCANVAS@ canvas) {
	canvas.drawSprite((432 * 32), (35 * 32) + 16, ANIM::CUSTOM[5], 0, 0, 0, SPRITE::BLEND_NORMAL, emoteFade);
	if (drawBirdie) canvas.drawSprite((447 * 32), (71 * 32) + 10, ANIM::CHUCK, 18, (jjGameTicks/12) & 3, -1);
}

// applying canopy to tree layer 1:
void onDrawLayer6(jjPLAYER@ player, jjCANVAS@ canvas) {
	for (int i = -1; i < 9; i++) canvas.drawSprite(0 + (960 * i), 0, ANIM::HELMUT, 0, 0); // you might want a bigger number of loops if your level is very wide
}

// applying canopy to tree layer 2:
void onDrawLayer7(jjPLAYER@ player, jjCANVAS@ canvas) {
	for (int i = -1; i < 7; i++) canvas.drawSprite(0 + (800 * i), 64, ANIM::HELMUT, 0, 1); // two tiles down for this level
}

class CustomBossHealthBar {
    int maxHealth;
    bool ready = false;
    jjOBJ@ boss;
    jjPIXELMAP healthBar(jjAnimFrames[jjAnimations[jjAnimSets[ANIM::BOSS].firstAnim].firstFrame + 1]);
    
    void initialize(jjOBJ@ boss)
    {
        @this.boss = @boss;
        maxHealth = boss.energy;
        ready = true;
    }
    
    void draw(jjCANVAS@ canvas)
    {
        if (boss.energy <= 0 || devanWait == true) return;
        
        if (bossPhase <= 3) canvas.drawString(jjResolutionWidth / 2 + 90, 20, "x" + (5 - bossPhase), STRING::SMALL, STRING::BOUNCE);
        
        for (int x = 0; x < 1 + int(ceil((healthBar.width * bossHealth) / maxHealth)); ++x)
            for (uint y = 0; y < healthBar.height; ++y)
                if (healthBar[x, y] != 0)
                    canvas.drawPixel(jjResolutionWidth / 2 + x - 69, 4 + y + 17, healthBar[x, y] + 16 * (4 - bossPhase));
    }
}

void tweakDevanRemoteAnim() {
    for (uint i = 0; i < jjAnimations[jjAnimSets[ANIM::DEVAN].firstAnim + 1].frameCount; ++i) {
        jjAnimFrames[jjAnimations[jjAnimSets[ANIM::DEVAN].firstAnim + 1].firstFrame + i].hotSpotY += 12;
    }
}

const array<SOUND::Sample> ButtstompSounds = {SOUND::COMMON_SPLAT1, SOUND::COMMON_SPLAT2, SOUND::COMMON_SPLAT3, SOUND::COMMON_SPLAT4};
class DDevan : jjBEHAVIORINTERFACE {
    int prevFrameID, physMoveIFrames, turnAroundCounter, cutsceneCounter;
    int crouchShootCounter, crouchShootFireCounter;
    jjOBJ@ bigGunProp;
    
    bool isLizardInitialized;
    float lizardRelXPos, lizardRelYPos, lizardTargetXPos, lizardTargetYPos;
    int lizardDirection, lizardTimer;
 
    int turnAroundTime = 50, animSpeed = 6, lizardWait = 128;
 
    DDevan(jjOBJ@ preset) {
        preset.behavior = this;
        preset.playerHandling = HANDLING::SPECIAL;
        preset.bulletHandling = HANDLING::DETECTBULLET;
        preset.scriptedCollisions = true;
        preset.isTarget = true;
        preset.isFreezable = true;
        preset.triggersTNT = true;
        
        initializeBoss();
    }
    
    void initializeBoss() {
        bossPhase = 1;
        bossHealth = 100;
        prevFrameID = -1;
        physMoveIFrames = 0;
        turnAroundCounter = 0;
        cutsceneCounter = 0;
        crouchShootCounter = 0;
        crouchShootFireCounter = 0;
        isLizardInitialized = false;
        @bigGunProp = null;
    }
    
    void initializeLizard(jjOBJ@ obj) {
        if (isLizardInitialized) return;
        
        lizardRelXPos = 512 * obj.direction;
        lizardRelYPos = lizardRelXPos * lizardRelXPos / 3072 - 160;
        lizardTargetXPos = obj.xPos;
        lizardTargetYPos = obj.yPos;
        lizardDirection = -obj.direction;
        lizardTimer = 0;
        
        isLizardInitialized = true;
    }
    
    void processLizard(jjOBJ@ obj) {
        if (!isLizardInitialized) return;
        
        lizardRelYPos = -lizardRelXPos * lizardRelXPos / 3072 - 160;
        if (lizardRelXPos > -96 && lizardRelXPos < 96 && lizardTimer <= lizardWait) lizardTimer++;
        else lizardRelXPos += 4 * lizardDirection;
        
        if (lizardTimer == 12 * animSpeed) {
            @bigGunProp = jjObjects[jjAddObject(OBJECT::BOMB, lizardRelXPos + lizardTargetXPos + 28 * lizardDirection, lizardRelYPos + lizardTargetYPos - 40)];
            bigGunProp.behavior = GunProp();
            bigGunProp.var[1] = ANIM::CUSTOM[19];
            bigGunProp.var[2] = 3;
            bigGunProp.direction = lizardDirection;
            bigGunProp.xSpeed = float(3 * lizardDirection) / 4;
            bigGunProp.ySpeed = -2;
        }
    }
    
    void drawLizard(jjOBJ@ obj) {
        if (!isLizardInitialized) return;
        
        jjDrawSprite(lizardRelXPos + lizardTargetXPos, lizardRelYPos + lizardTargetYPos, ANIM::LIZARD, 3, jjGameTicks >> 2, lizardDirection, SPRITE::BRIGHTNESS, 110, -1);
        
        if (lizardTimer > 0 && lizardTimer < 13 * animSpeed)
            jjDrawSprite(lizardRelXPos + lizardTargetXPos, lizardRelYPos + lizardTargetYPos, ANIM::CUSTOM[19], 11, lizardTimer / animSpeed, lizardDirection, SPRITE::BRIGHTNESS, 110, -1);
        else
            jjDrawSprite(lizardRelXPos + lizardTargetXPos, lizardRelYPos + lizardTargetYPos, ANIM::LIZARD, 2, jjGameTicks >> 3, lizardDirection, SPRITE::BRIGHTNESS, 110, -1);
    }
    
    void usePhaseTwoAnims(jjOBJ@ obj) {
        if (uint(obj.curAnim) == jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 13) { obj.determineCurAnim(ANIM::CUSTOM[19], 1); }
        else if (uint(obj.curAnim) == jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 14) { obj.determineCurAnim(ANIM::CUSTOM[19], 2); }
        else if (uint(obj.curAnim) == jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 16) { obj.determineCurAnim(ANIM::CUSTOM[19], 4); }
        else if (uint(obj.curAnim) == jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 18) { obj.determineCurAnim(ANIM::CUSTOM[19], 5); }
        else if (uint(obj.curAnim) == jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 19) { obj.determineCurAnim(ANIM::CUSTOM[19], 6); }
        else if (uint(obj.curAnim) == jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 20) { obj.determineCurAnim(ANIM::CUSTOM[19], 7); }
        else if (uint(obj.curAnim) == jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 21) { obj.determineCurAnim(ANIM::CUSTOM[19], 8); }
        else if (uint(obj.curAnim) == jjAnimSets[ANIM::CUSTOM[19]].firstAnim + 9) { obj.determineCurAnim(ANIM::CUSTOM[19], 10); }
    }
    
     void replacePhaseOneProjectiles() {
        for (int i = 0; i < jjObjectCount; i++) { 
            jjOBJ@ obj = jjObjects[i];
            if (uint(obj.curAnim) == jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 17 && jjGameTicks % 1 == 0) {
                jjOBJ@ laser = jjObjects[obj.fireBullet(OBJECT::LASER)];
                laser.behavior = BEHAVIOR::BULLET;
                laser.direction = jjObjects[obj.creatorID].direction;
                if (jjObjects[i].creatorID == uint(obj.objectID)) jjObjects[i].state = STATE::KILL;
                laser.creatorID = obj.creatorID;
                laser.creatorType = CREATOR::OBJECT;
                laser.playerHandling = HANDLING::ENEMYBULLET;
                
                int playerID = obj.findNearestPlayer(2000000);
                if (playerID >= 0 && laser.direction * jjLocalPlayers[playerID].xPos > laser.direction * laser.xPos && abs(jjLocalPlayers[playerID].yPos - laser.yPos) <= 32.f)
                    jjLocalPlayers[playerID].hurt();

                obj.delete();
            }
        }
    }
    
    void replacePhaseThreeProjectiles() {
        for (int i = 0; i < jjObjectCount; i++) { 
            jjOBJ@ obj = jjObjects[i];
            if (obj.curAnim == int(jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 4) && obj.counter == 0 && jjGameTicks % 1 == 0) {
                jjOBJ@ bilsyFire = jjObjects[obj.fireBullet(OBJECT::BULLET)];
                bilsyFire.behavior = BEHAVIOR::BILSYBULLET;
                bilsyFire.direction = obj.direction;
                bilsyFire.creatorID = obj.objectID;
                bilsyFire.creatorType = CREATOR::OBJECT;
                bilsyFire.playerHandling = HANDLING::ENEMYBULLET;
                bilsyFire.animSpeed = 2;
                bilsyFire.determineCurAnim(ANIM::BILSBOSS, 3);
 
                obj.delete();
            }
        }
    }
    
    void onBehave(jjOBJ@ obj) {
        // 1 - SLEEP - Defeated, sitting
        // 2 - WAKE - Transform into D. Devan
        // 5 - WALK - Walking
        // 6 - JUMP - Jumping
        // 7 - FIRE - Shooting
        // 8 - FLY - (DD) Fly
        // 12 - STILL - Idle
        // 17 - DONE - Defeated, falling
        // 19 - FALL - Falling
        // 22 - ATTACK - (DD) Shoot fireballs
        // 24 - FADEIN - Teleporting in
        // 31 - WAIT - ?
        // 29 - EXTRA - Recover from falling
        // 33 - DELAYEDSTART - Starting
        // 35 - DUCK - Ducking
        
        // Bonus states:
        // 9 - BOUNCE - Crouch shooting
		// 13 - FLOAT - Move to final position, wait for end cutscene
        // 27 - TURN - Phase 1 to 2 cutscene
        
        int playerID = obj.findNearestPlayer(1000000);
        float finalXPos, dirX, dirY, length;
		float finalYPos = 19 * 32;
        obj.behave(BEHAVIOR::DEVILDEVAN, false);
        
        if (physMoveIFrames > 0) physMoveIFrames--;
        if (obj.freeze > 35) obj.freeze = 35;
        if (obj.state != STATE::WALK) turnAroundCounter = 0;
        if (bossPhase == 1 && bossHealth <= 0) {
            float yAux = obj.yPos;
            obj.putOnGround();
            // Making extra sure that Devan is on the ground when he's losing his gun.
            // Hopefully this approach doesn't cause any issues :P
            if (yAux == obj.yPos) {
                obj.state = STATE::TURN;
                obj.isFreezable = false;
                bossPhase = 2;
                bossHealth = 100;
                crouchShootCounter = 0;
                crouchShootFireCounter = 0;
            }
            else obj.yPos = yAux;
        }
        
        obj.energy = (bossHealth <= 0) ? 1 : bossHealth;
        
        if (bossPhase == 4 && bossHealth <= 0) {
            if (obj.state == STATE::ATTACK) { // Dying while shooting fireballs seems to cause issues.
                obj.determineCurAnim(ANIM::DEVILDEVAN, 5);
                obj.state = STATE::FLY; 
            }

            //bossPhase = 5;
            obj.energy = 0;
            obj.state = STATE::FLOAT;
            if (playerID >=0) givePlayerPointsForObject(jjLocalPlayers[playerID], obj);
        }
        
        if (bossPhase == 5) { 
            obj.var[2] = 69; // Nice.
            if (playerID >= 0 && jjLocalPlayers[playerID].boss == 0) {
                obj.determineCurAnim(ANIM::DEVILDEVAN, 11);
                if (prevFrameID == -1) {
                    obj.frameID = 0;
                    prevFrameID = 0;
                    obj.determineCurFrame();
                    jjSample(obj.xPos, obj.yPos, SOUND::COMMON_TELPORT2);
                }
                if (prevFrameID == 6 && obj.frameID != prevFrameID) obj.delete();
                prevFrameID = obj.frameID;
            }
        }
        
        switch (obj.state) {
            case STATE::WALK:
                if (playerID >= 0) { 
                    if (obj.direction * jjLocalPlayers[playerID].xPos < obj.direction * obj.xPos)
                        turnAroundCounter++;
                    if (turnAroundCounter >= turnAroundTime) {
                        turnAroundCounter = 0;
                        obj.direction *= -1;
                    }
                }
                
                break;
            
            case STATE::DUCK:
                if (obj.frameID == 5) {
                    if (bossPhase == 1) obj.determineCurAnim(ANIM::CUSTOM[19], 9);
                    else obj.determineCurAnim(ANIM::CUSTOM[19], 10);
                    obj.frameID = 0;
                    obj.state = STATE::BOUNCE;
                }
                
                break;
            
            case STATE::FIRE:
                if (playerID >= 0 && (obj.frameID == 4 || obj.frameID == 5) && (jjLocalPlayers[playerID].curAnim - jjAnimSets[jjLocalPlayers[playerID].setID].firstAnim == RABBIT::DIVE) || (jjLocalPlayers[playerID].curAnim - jjAnimSets[jjLocalPlayers[playerID].setID].firstAnim == RABBIT::DIVEFIRERIGHT)) {
                    obj.frameID = 0;
                    obj.state = STATE::DUCK;
                }
                
                break;
                
            case STATE::BOUNCE:
                if (obj.frameID < 4) obj.frameID = int(crouchShootCounter / (animSpeed - 2));
                else if (obj.frameID == 4 || obj.frameID == 5) {
                    if (crouchShootCounter % (animSpeed - 2) == 0) {
                        if (playerID >= 0 && crouchShootFireCounter < 11 && ((obj.direction * jjLocalPlayers[playerID].xPos > obj.direction * obj.xPos && abs(jjLocalPlayers[playerID].yPos - obj.yPos) < 50) || crouchShootFireCounter < 1)) {
 
                            if (obj.frameID == 4) obj.frameID = 5;
                            else if (obj.frameID == 5) { 
                                obj.frameID = 4;
								jjSample(obj.xPos, obj.yPos, SOUND::DEVILDEVAN_PHASER2);
 
                                jjOBJ@ blast = jjObjects[obj.fireBullet(OBJECT::BLASTERBULLET)];
                                blast.determineCurAnim(ANIM::DEVILDEVAN, 17);
                                blast.playerHandling = HANDLING::ENEMYBULLET;
                                blast.state = STATE::FLY; 
                                blast.counterEnd = 63;
                                blast.direction = obj.direction;
                                blast.xSpeed = 6 * obj.direction;
                                blast.xAcc = 0;
                                blast.determineCurFrame();
 
                                crouchShootFireCounter++;
                            }
                        }
                        else obj.frameID++;
                    }
                }
                else if (crouchShootCounter % (animSpeed - 2) == 0) obj.frameID++;
 
                crouchShootCounter++;
 
                if (obj.frameID == 9 && crouchShootCounter % (animSpeed - 2) == 3) {
                    crouchShootCounter = 0;
                    crouchShootFireCounter = 0;
 
                    obj.determineCurAnim(ANIM::DEVILDEVAN, 13);
                    obj.frameID = 7;
                    obj.state = STATE::DUCK;
                }
 
                break;
                
            case STATE::TURN:
                if (bossPhase == 2) {
                    if (cutsceneCounter == 0 && playerID >= 0) jjLocalPlayers[playerID].cameraFreeze(obj.xPos, obj.yPos, true, false); 
                    if (cutsceneCounter < animSpeed * 7) {
                        obj.determineCurAnim(ANIM::DEVILDEVAN, 12);
                        obj.frameID = cutsceneCounter / animSpeed;
                        if (cutsceneCounter == animSpeed) {
                            jjOBJ@ gun = jjObjects[jjAddObject(OBJECT::BOMB, obj.xPos + 25 * -obj.direction, obj.yPos - 10)];
                            gun.behavior = GunProp();
                            gun.var[1] = ANIM::DEVILDEVAN;
                            gun.var[2] = 15;
                            gun.direction = -obj.direction;
                            gun.xSpeed = -3 * obj.direction;
                            gun.ySpeed = -2;
                        }
                    }
                    else if (cutsceneCounter < animSpeed * 7 + animSpeed * 14) {
                        obj.determineCurAnim(ANIM::CUSTOM[19], 0);
                        obj.frameID = cutsceneCounter / animSpeed - 7;
                    }
                    else {
                        initializeLizard(obj);
                        
                        obj.determineCurAnim(ANIM::DEVAN, 1);
                        obj.frameID = jjGameTicks >> 3;
                    }
                
                    cutsceneCounter++;
                }
            
            break;
			
			case STATE::FLOAT:
				if (obj.xPos >= 488 * 32) finalXPos = 493 * 32;
				else finalXPos = 481 * 32;
				if (obj.xPos > jjLocalPlayers[playerID].xPos) obj.direction = -1;
				else obj.direction = 1;
				obj.determineCurAnim(ANIM::DEVILDEVAN, 5);
				obj.frameID = ((jjGameTicks/10) % jjAnimations[obj.curAnim].frameCount);
				if (jjGameTicks % 10 == 0) obj.justHit = 5;
				if (abs(obj.xPos - finalXPos) <= 32.f && abs(obj.yPos - finalYPos) <= 32.f && jjLocalPlayers[playerID].yPos < 26 * 32 && (jjLocalPlayers[playerID].curAnim - jjAnimSets[jjLocalPlayers[playerID].setID].firstAnim == RABBIT::STAND || jjLocalPlayers[playerID].curAnim - jjAnimSets[jjLocalPlayers[playerID].setID].firstAnim == RABBIT::RUN1 || jjLocalPlayers[playerID].curAnim - jjAnimSets[jjLocalPlayers[playerID].setID].firstAnim == RABBIT::RUN2 || jjLocalPlayers[playerID].curAnim - jjAnimSets[jjLocalPlayers[playerID].setID].firstAnim == RABBIT::RUN3)) {
					bossPhase = 5;
					obj.state = STATE::DONE;
				}
				else {
					dirX = finalXPos - obj.xPos;
					dirY = finalYPos - obj.yPos;
					length = sqrt(dirX * dirX + dirY * dirY);
					
					if (length > 0) {
						dirX /= length;
						dirY /= length;
					}				
					obj.xPos += dirX;
					obj.yPos += dirY;
				}
				break;
        }
        
        if (bossPhase == 2) {
            usePhaseTwoAnims(obj);
            if (cutsceneCounter > 200) {
				replacePhaseOneProjectiles();
			}
            if (obj.state != STATE::STILL && bigGunProp !is null && obj.doesCollide(bigGunProp)) {
                bigGunProp.deactivate();
                @bigGunProp = null;
				jjSample(obj.xPos, obj.yPos, SOUND::DEVILDEVAN_LAUGH);
				jjSampleLoad(SOUND::DEVILDEVAN_PHASER2, "laser.wav"); // ghetto solution
                obj.determineCurAnim(ANIM::CUSTOM[19], 8);
                obj.state = STATE::STILL;
                obj.isFreezable = true;
                obj.counter = 0;
                if (playerID >= 0) jjLocalPlayers[playerID].cameraUnfreeze(false);
            }
            if (bossHealth <= 0) {
                bossHealth = 100;
                obj.state = STATE::WAKE;
                obj.frameID = 0;
                bossPhase = 3;
            }
        }
        
        if (bossPhase == 3 && bossHealth <= 0 && uint(obj.curAnim) != jjAnimSets[ANIM::DEVILDEVAN].firstAnim + 6) {
            bossHealth = 100;
            bossPhase = 4;
        }
        
        if (bossPhase == 4) replacePhaseThreeProjectiles();
        
        processLizard(obj);
    }
    
    void onDraw(jjOBJ@ obj) {
        if (obj.state == STATE::DELAYEDSTART) return;
        obj.determineCurFrame();
        
        drawLizard(obj);
 
        if (obj.state == STATE::FREEZE) {
            jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos + ((jjGameTicks % 8 < 4) ? 0 : 3), obj.curFrame, obj.direction, (jjGameTicks % 8 < 4) ? SPRITE::FROZEN : ((bossPhase == 4) ? SPRITE::PALSHIFT : SPRITE::NORMAL), (bossPhase == 4) ? 8 : 0);
        }
        else {
            if (obj.justHit == 0) 
                jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, (bossPhase == 4) ? SPRITE::PALSHIFT : SPRITE::NORMAL, (bossPhase == 4) ? 8 : 0);
            else jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::SINGLECOLOR, 15);
        }
    }
    
    bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
        // Edited version of the onObjectHit function of the Flower class from plus52Scripting.j2as
        if (bossPhase == 5 || obj.state == STATE::FLOAT) return true;
        
        if (bullet !is null) {
            //recreation of HANDLING::HURTBYBULLET with HANDLING::ENEMY
            
            if (obj.causesRicochet) {
                if ((bullet.var[6] & 6) == 0) //not fire-based, not a laser beam
                    bullet.ricochet();
                else if ((bullet.var[6] & 4) == 0) //not a laser beam
                    bullet.delete();
            } else if ((bullet.var[6] & 16) == 0) //not a fireball
                bullet.state = STATE::EXPLODE;
                
            if (obj.freeze > 0 && force < 3)
                force = 3;
                
            if (obj.state != STATE::TURN) { 
                bossHealth -= force;
                obj.justHit = 5; //flash white for 5 ticks--jjOBJ::justHit is automatically deincremented by the JJ2 engine, so individual behavior functions don't need to worry about doing that.
            }
            
            if (bossHealth <= 0) { //killed
                bossHealth = 0;
                if (obj.freeze > 0)
                    obj.unfreeze(0);
                if (player !is null) {
                    obj.grantPickup(player, (uint(bullet.curAnim) == jjAnimSets[ANIM::AMMO].firstAnim + 17) ? 5 : 10);
                }
            } else
                obj.freeze = 0;
                
        } else { //recreation of HANDLING::ENEMY; player guaranteed to be non-null
            if (force != 0 && physMoveIFrames == 0) { //attacking via special attack, e.g. buttstomp
                if (obj.state != STATE::TURN) bossHealth -= 4; //constant amount of damage for special attacks
                physMoveIFrames = 10;
 
                if (obj.state != STATE::TURN && bossHealth > 0) { //only wounded
                    obj.justHit = 5;
                }
                
                if (obj.freeze > 0) {
                    obj.unfreeze(1);
                } else {
                    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);
                }
            } else if (!(obj.freeze > 0)) { //not attacking
                player.hurt();
            }
        }
        return true;
    }
 
}
 
class GunProp : jjBEHAVIORINTERFACE {
    void onBehave(jjOBJ@ obj) {
        if (obj.var[0] == 0) initialize(obj);
        if (obj.state == STATE::FLY && obj.counter < 200) {
            obj.xPos += obj.xSpeed;
            obj.yPos += obj.ySpeed;
            obj.ySpeed += 0.125;
            obj.counter++;
//          jjPrint(""+obj.counter);
        }
        else obj.delete();
    }
    
    void initialize(jjOBJ@ obj) {
        obj.var[0] = 1;
        obj.playerHandling = HANDLING::PARTICLE;
        obj.bulletHandling = HANDLING::IGNOREBULLET;
        obj.isTarget = false;
        obj.isFreezable = false;
        obj.isBlastable = false;
        obj.triggersTNT = false;
        obj.state = STATE::FLY;
        obj.lightType = LIGHT::NONE;
    }
    
    void onDraw(jjOBJ@ obj) {
        jjDrawSprite(obj.xPos, obj.yPos, obj.var[1], obj.var[2], 8 - jjGameTicks >> 2, obj.direction, SPRITE::NORMAL, 0, -1);
    }
}

bool givePlayerPointsForObject(jjPLAYER@ player, jjOBJ@ obj) { 
    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; 
        return true;
    }
    return false;
}

class shamanDemon : jjBEHAVIORINTERFACE {
int curseTarget;
int curseDistance = (256 * 256) + (jjDifficulty * 256 * 64);
int lastHitID;
	void onBehave(jjOBJ@ obj) {
		switch (obj.state) {
		case STATE::START:
			obj.putOnGround();
			curseTarget = -2; // clear targets after reloading
			obj.state = STATE::WALK;
		case STATE::WALK:
			obj.behave(BEHAVIOR::WALKINGENEMY);
			obj.var[3] = 0; // for summon animation
			obj.var[4] = 0; // for bonk animation
			if (obj.direction >= 0) {
				obj.xSpeed = 0.2;
			} else { obj.xSpeed = -0.2; }
			if (obj.findNearestPlayer(curseDistance) > -1 && obj.findNearestPlayer(curseDistance) != curseTarget) { // only one cloud per player can be active FROM THE SAME SHAMAN DEMON
				curseTarget = obj.findNearestPlayer(curseDistance);
				obj.state = STATE::EXTRA;
			}
			obj.var[2] = obj.var[2] - 1;
			if (jjRandom() % 512 == 0 && obj.var[2] <= 0) {
				obj.var[1] = 70 + (jjRandom() % 140); // time of idling
				obj.state = STATE::IDLE;
			}
			if (obj.findNearestPlayer(5000) > -1) {
				if (obj.xPos > jjLocalPlayers[obj.findNearestPlayer(5000)].xPos) { obj.direction = -1; }
				else { obj.direction = 1; }
				obj.state = STATE::ATTACK;
			}
			break;
		case STATE::IDLE:
			if (obj.var[1] == 0) {
				int rand = jjRandom()%2;
				obj.direction = rand - 1; // randomly change direction
				if (obj.direction >= 0) {
				obj.xSpeed = 0.2;
				} else { obj.xSpeed = -0.2; } // have to declare movement vector again or direction will never change here for some reason...
				obj.var[2] = 140; // idle cooldown
				obj.state = STATE::WALK;
			} else {
				obj.var[1] = obj.var[1] - 1;
			}
			break;
		case STATE::ATTACK:
			obj.var[4] = obj.var[4] + 1;
			if (obj.var[4] == 42) jjSample(obj.xPos, obj.yPos, SOUND::INTRO_HITTURT);
			if (obj.var[4] == 70) obj.state = STATE::WALK;
			break;
		case STATE::EXTRA:
			obj.var[3] = obj.var[3] + 1;
			if (obj.var[3] == 20) {
				int rand = jjRandom()%2;
				if (rand==0)
				jjSample(obj.xPos, obj.yPos, SOUND::INTRO_INHALE);
				else jjSample(obj.xPos, obj.yPos, SOUND::INTRO_UHTURT);
			}
			if (obj.var[3] == 90) {
				jjOBJ@ cloud = jjObjects[jjAddObject(OBJECT::RAPIER, jjLocalPlayers[curseTarget].xPos, jjLocalPlayers[curseTarget].yPos - 128, obj.objectID)];
				cloud.var[2] = curseTarget;
				int rand = jjRandom()%2;
				if (rand==0)
				jjSample(cloud.xPos, cloud.yPos, SOUND::AMMO_BLUB1);
				else jjSample(cloud.xPos, cloud.yPos, SOUND::AMMO_BLUB2);
				for( int n = 0; n < 2 + int(jjRandom()%3); n++ ) {
				jjOBJ@ spark = jjObjects[jjAddObject(OBJECT::SHARD, cloud.xPos, cloud.yPos, cloud.objectID, CREATOR::OBJECT)];
				spark.determineCurAnim(ANIM::PICKUPS, 27);
				}
			}
			if (obj.var[3] == 112) {
				obj.state = STATE::WALK;
			}
			break;
		case STATE::FREEZE:
			if (obj.freeze == 0) obj.state = obj.oldState;
			else obj.freeze--;
			break;
		case STATE::DEACTIVATE:
			for (int i = 0; i < jjObjectCount; ++i)
			if (int(jjObjects[i].creatorID) == obj.objectID) jjObjects[i].state = STATE::KILL;
			obj.deactivate();
			break;
		case STATE::KILL:
			for (int i = 0; i < jjObjectCount; ++i)
			if (int(jjObjects[i].creatorID) == obj.objectID) jjObjects[i].state = STATE::KILL;
			givePlayerPointsForObject(jjLocalPlayers[lastHitID], obj);
			obj.particlePixelExplosion(0);
			obj.delete();
		}
	}
	void onDraw(jjOBJ@ obj) {
		switch (obj.state) {
		case STATE::IDLE:
			obj.determineCurAnim(ANIM::CUSTOM[9], 0);
			obj.frameID = ((jjGameTicks/9) % jjAnimations[obj.curAnim].frameCount);
			break;
		case STATE::WALK:
			obj.determineCurAnim(ANIM::CUSTOM[9], 1);
			obj.frameID = ((jjGameTicks/9) % jjAnimations[obj.curAnim].frameCount);
			break;
		case STATE::ATTACK:
			obj.determineCurAnim(ANIM::CUSTOM[9], 2);
			obj.frameID = int8(obj.var[4] / 7);
			break;
		case STATE::EXTRA:
			obj.determineCurAnim(ANIM::CUSTOM[9], 3);
			obj.frameID = int8(obj.var[3] / 7);
			break;
		}
		obj.determineCurFrame();
		obj.draw();
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (bullet !is null) {
			if ((bullet.var[6] & 16) == 0) { //not a fireball
				bullet.state = STATE::EXPLODE;
			}
			if (obj.freeze != 0) {
				if (force < 3)
					force = 3;
				obj.unfreeze(0);
			}
			if (bullet.var[3] == 8) obj.energy -= (bullet.animSpeed * 2); // super effective
			else obj.energy -= bullet.animSpeed;
			obj.justHit = 5;
		} else {
			if (force == 0) { //collision
				if (obj.state != STATE::FREEZE)
					player.hurt();
			} else {
				if (force == 1) { //sugar rush/buttstomp
					bool bounce = true;
					if (player.buttstomp == 41) //actually stomping
						bounce = !player.hurt(); //stomping but protected by something, including a sugar rush
					if (bounce) {
						player.buttstomp = 50; //landing
						player.ySpeed = player.ySpeed / -2 - 8;
						player.yAcc = 0;
						player.extendInvincibility(-70);
					}
				} else if (force == -101) { //frozen+running
					player.xAcc = 0;
					player.xSpeed /= -2;
					player.ySpeed = -6;
					player.extendInvincibility(-10);
				}
				if (obj.var[7] < jjGameTicks) { //hasn't been hurt recently
					obj.var[7] = jjGameTicks + 70; //next time this can be hurt by a physical attack
					if (obj.freeze != 0)
						obj.unfreeze(1);
					else
						jjSample(obj.xPos, obj.yPos, ButtstompSounds[jjRandom() % ButtstompSounds.length]);
					obj.energy -= 4;
					obj.justHit = 5;
				}
			}
		}
		lastHitID = player.playerID;
		if (obj.energy <= 0) obj.state = STATE::KILL;
		return true;
	}
}

class stormCloud : jjBEHAVIORINTERFACE {
float followSpeed = 5 + jjDifficulty;
int attackCD = 240 - (jjDifficulty * 40);
	void onBehave(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::START:
				obj.var[1] = attackCD;
				obj.state = STATE::IDLE;
			case STATE::IDLE:
				obj.var[3] = 50 - (jjDifficulty * 10);
				obj.frameID = (jjGameTicks/5) & 3;
				obj.determineCurFrame();
				if (abs(obj.xPos - jjLocalPlayers[obj.var[2]].xPos) > 8.f || abs(obj.yPos - (jjLocalPlayers[obj.var[2]].yPos - 128)) > 8.f) {
					float dirX = jjLocalPlayers[obj.var[2]].xPos - obj.xPos;
					float dirY = jjLocalPlayers[obj.var[2]].yPos - 128 - obj.yPos;
					float length = sqrt(dirX * dirX + dirY * dirY);
					
					if (length > 0) {
						dirX /= length;
						dirY /= length;
					}
					
					obj.xPos += dirX * followSpeed;
					obj.yPos += dirY * followSpeed;
				}
				obj.var[1] = obj.var[1] - 1;
				if (obj.var[1] <= 0) {
					obj.var[2] = 40;
					obj.state = STATE::ATTACK;
				}
				break;
			case STATE::ATTACK:
				obj.frameID = (jjGameTicks/3) & 3;
				obj.determineCurFrame();
				if (jjGameTicks % 10 == 0) { obj.justHit = 5; }
				obj.var[3] = obj.var[3] - 1;
				if (obj.var[3] <= 0) {
					jjSample(obj.xPos, obj.yPos, SOUND::COMMON_ELECTRIC1);
					jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::LIGHTNINGSHIELDBULLET)];
					bullet.ySpeed = 5;
					bullet.creatorID = obj.objectID;
					bullet.creatorType = CREATOR::OBJECT;
					bullet.playerHandling = HANDLING::ENEMYBULLET;
					obj.var[1] = attackCD;
					obj.state = STATE::IDLE;
					}
				break;
			case STATE::DEACTIVATE:
				obj.delete();
				break;
			case STATE::KILL:
				if (obj.isActive) {
					jjOBJ@ poof = jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos)];
					poof.determineCurAnim(ANIM::AMMO, 2);
				}
				obj.delete();
				break;
		}
	}
	void onDraw(jjOBJ@ obj) {
		if (obj.state == STATE::ATTACK) {
			if (obj.justHit > 0) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 1, SPRITE::SINGLECOLOR, 15, 1);
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 1, SPRITE::NORMAL, 0, 1);
		}
		else jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 1, SPRITE::TRANSLUCENT, 0, 1);
	}
}

class guerrillaDoofus : jjBEHAVIORINTERFACE {
// OBJECT VARIABLES:
// var[1] = jump stamina
// var[2] = blaster power
// var[3] = animation counter (start/end jump, start/end laying down)
// var[4] = animation counter (peak of jump)
// var[5] = idle counter
// var[6] = turning stuck check
	void onBehave(jjOBJ@ obj) {
	int count = jjRandom() % 3 + 8;
		switch (obj.state) {
		case STATE::START:
			obj.var[6] = 0;
			obj.state = STATE::SLEEP;
		case STATE::SLEEP: // hiding in bush
			obj.playerHandling = HANDLING::PARTICLE;
			obj.bulletHandling = HANDLING::IGNOREBULLET;
			obj.determineCurAnim(ANIM::CUSTOM[22], 12);
			obj.frameID = ((jjGameTicks/7) % jjAnimations[obj.curAnim].frameCount);
			obj.xPos = obj.xOrg;
			obj.yPos = obj.yOrg;
			if (obj.findNearestPlayer(256 * 128) > -1) {
				if (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].xPos < obj.xPos) obj.direction = -1;
				else obj.direction = 1;		
				for (int i = 0; i < count; i++) {
					jjPARTICLE@ particle = jjAddParticle(PARTICLE::LEAF);
					particle.xPos = obj.xPos;
					particle.yPos = obj.yPos;
					particle.xSpeed = int(jjRandom() % 9 - 4);
					particle.ySpeed = int(jjRandom() % 4 - 4);
				}
				jjSample(obj.xPos, obj.yPos, SOUND::INTRO_GREN2);
				obj.xSpeed = obj.direction * 1.2;
				obj.ySpeed = -3;
				obj.state = STATE::JUMP;
			}
			break;
			
		case STATE::WALK:
			if (obj.var[6] > 0) obj.var[6] = obj.var[6] - 1;
			obj.xPos = obj.xPos + obj.xSpeed;
			obj.determineCurAnim(ANIM::CUSTOM[22], 8);
			obj.var[3] = 0;
			obj.frameID = ((jjGameTicks/7) % jjAnimations[obj.curAnim].frameCount);
			
			// turning check:
			if (!jjMaskedVLine(int(obj.xPos + obj.xSpeed), int(obj.yPos) + 27, 4) || jjMaskedVLine(int(obj.xPos + (obj.xSpeed * 8)), int(obj.yPos) - 8, 30) || jjEventGet(uint16(obj.xPos / 32), uint16(obj.yPos / 32)) == 14) {
				obj.var[4] = 14;
				obj.state = STATE::TURN;
			}
			
			// idling check:
			obj.var[5] = obj.var[5] - 1;
			if (jjRandom() % 512 == 0 && obj.var[5] <= 0) {
				obj.var[5] = 70 + (jjRandom() % 140); // time of idling
				obj.state = STATE::IDLE;
			}
			
			// duck check:
			if ((jjLocalPlayers[obj.findNearestPlayer(256 * 128)].yPos >= obj.yPos - 48 && obj.direction * (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].xPos - obj.xPos) > 0 && (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].curAnim - jjAnimSets[jjLocalPlayers[obj.findNearestPlayer(256 * 128)].setID].firstAnim == RABBIT::DIVE) || (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].curAnim - jjAnimSets[jjLocalPlayers[obj.findNearestPlayer(256 * 128)].setID].firstAnim == RABBIT::DIVEFIRERIGHT)) || (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].yPos > obj.yPos + 90 && obj.direction * (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].xPos - obj.xPos) > 0)) {
				obj.var[3] = 0;
				obj.state = STATE::DUCK;
			}
			
			// jump check:
			if (obj.var[1] > 0) obj.var[1] = obj.var[1] - 1;
			if (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].yPos < obj.yPos - 48 && obj.var[1] == 0 && obj.direction * (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].xPos - obj.xPos) > 0) obj.state = STATE::SPRING;
			
			// fire check:
			if (obj.var[2] < 6 && jjGameTicks % 22 == 0) obj.var[2] = obj.var[2] + 1;
			else if ((jjLocalPlayers[obj.findNearestPlayer(256 * 128)].yPos < obj.yPos + 80 || jjLocalPlayers[obj.findNearestPlayer(256 * 128)].yPos >= obj.yPos + 1600) && obj.var[2] > 0 && obj.direction * (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].xPos - obj.xPos) > 0) {
				if (jjGameTicks % 10 == 0) {
					jjSample(obj.xPos, obj.yPos, SOUND::DEVILDEVAN_PHASER2, 63, 13081);
					jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::FIREBALLBULLET)];
					jjOBJ@ flash = jjObjects[jjAddObject(OBJECT::EXPLOSION, bullet.xPos, bullet.yPos)];
					flash.determineCurAnim(ANIM::AMMO, 16);
					bullet.determineCurAnim(ANIM::DEVAN, 0);
					bullet.creatorID = obj.objectID;
					bullet.creatorType = CREATOR::OBJECT;
					bullet.playerHandling = HANDLING::ENEMYBULLET;
					bullet.direction = obj.direction;
					bullet.animSpeed = 1;
					bullet.xSpeed = obj.xSpeed * 2;
					if (bullet.direction < 0) {
						bullet.xSpeed *= -1;
						bullet.xAcc *= -1;
						flash.direction = -1;
						}
					else flash.direction = 1;
					obj.var[2] = obj.var[2] - 1;
				}
			}
			break;
			
		case STATE::IDLE:
			// jump check:
			if (obj.var[1] > 0) obj.var[1] = obj.var[1] - 1;
			if (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].yPos < obj.yPos - 48 && obj.var[1] == 0 && obj.direction * (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].xPos - obj.xPos) > 0) obj.state = STATE::SPRING;
			
			// fire check:
			if (obj.var[2] < 6 && jjGameTicks % 22 == 0) obj.var[2] = obj.var[2] + 1;
			if (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].yPos < obj.yPos + 80 && obj.var[2] > 0 && obj.direction * (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].xPos - obj.xPos) > 0) {
				obj.determineCurAnim(ANIM::CUSTOM[22], 7);
				if (jjGameTicks % 10 == 0) {
					jjSample(obj.xPos, obj.yPos, SOUND::DEVILDEVAN_PHASER2, 63, 13081);
					jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::FIREBALLBULLET)];
					jjOBJ@ flash = jjObjects[jjAddObject(OBJECT::EXPLOSION, bullet.xPos, bullet.yPos)];
					flash.determineCurAnim(ANIM::AMMO, 16);
					bullet.determineCurAnim(ANIM::DEVAN, 0);
					bullet.creatorID = obj.objectID;
					bullet.creatorType = CREATOR::OBJECT;
					bullet.playerHandling = HANDLING::ENEMYBULLET;
					bullet.direction = obj.direction;
					bullet.animSpeed = 1;
					bullet.xSpeed = obj.xSpeed * 2;
					if (bullet.direction < 0) {
						bullet.xSpeed *= -1;
						bullet.xAcc *= -1;
						flash.direction = -1;
						}
					else flash.direction = 1;
					obj.var[2] = obj.var[2] - 1;
				}
			}
			else obj.determineCurAnim(ANIM::CUSTOM[22], 5);
			obj.frameID = ((jjGameTicks/7) % jjAnimations[obj.curAnim].frameCount);
			
			// walk check:
			if (obj.var[5] <= 0 && uint(obj.curAnim) != jjAnimations[jjAnimSets[ANIM::CUSTOM[22] + 7]]) { // not busy shooting?
				obj.var[5] = 140 + (jjRandom() % 140); // time until next idle
				obj.state = STATE::WALK;
			}
			else obj.var[5] = obj.var[5] - 1;
			break;
		
		case STATE::DUCK: // start of laying down
			obj.determineCurAnim(ANIM::CUSTOM[22], 9);
			obj.frameID = int8(obj.var[3] / 7);
			obj.var[3] = obj.var[3] + 1;
			if (obj.var[3] == 20) {
				obj.state = STATE::HIDE;
			}
			break;
			
		case STATE::HIDE: // laying down
			if (obj.var[3] > 0 && jjGameTicks % 10 == 0) obj.var[3] = obj.var[3] - 1; // don't get up too fast
			obj.determineCurAnim(ANIM::CUSTOM[22], 10);
			
			// fire check:
			if (obj.var[2] < 6 && jjGameTicks % 22 == 0) obj.var[2] = obj.var[2] + 1;
			if (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].yPos >= obj.yPos - 4 && jjLocalPlayers[obj.findNearestPlayer(256 * 128)].yPos < obj.yPos + 90 && obj.var[2] > 0 && obj.direction * (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].xPos - obj.xPos) > 0) {
				obj.frameID = ((jjGameTicks/7) % jjAnimations[obj.curAnim].frameCount);
				if (jjGameTicks % 10 == 0) {
					jjSample(obj.xPos, obj.yPos, SOUND::DEVILDEVAN_PHASER2, 63, 13081);
					jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::FIREBALLBULLET)];
					jjOBJ@ flash = jjObjects[jjAddObject(OBJECT::EXPLOSION, bullet.xPos, bullet.yPos)];
					flash.determineCurAnim(ANIM::AMMO, 16);
					bullet.determineCurAnim(ANIM::DEVAN, 0);
					bullet.creatorID = obj.objectID;
					bullet.creatorType = CREATOR::OBJECT;
					bullet.playerHandling = HANDLING::ENEMYBULLET;
					bullet.direction = obj.direction;
					bullet.animSpeed = 1;
					bullet.xSpeed = obj.xSpeed * 2;
					if (bullet.direction < 0) {
						bullet.xSpeed *= -1;
						bullet.xAcc *= -1;
						flash.direction = -1;
						}
					else flash.direction = 1;
					obj.var[2] = obj.var[2] - 1;
				}
			}
			else obj.frameID = 0;
			
			// get up check:
			if ((jjLocalPlayers[obj.findNearestPlayer(256 * 128)].yPos < obj.yPos - 4 || obj.findNearestPlayer(256 * 160) < 0 || (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].xPos > obj.xPos && obj.direction < 0) || (jjLocalPlayers[obj.findNearestPlayer(256 * 128)].xPos < obj.xPos && obj.direction >= 0)) && jjGameTicks % 22 == 0 && obj.var[3] <= 0) {
				obj.var[3] = 0;
				obj.state = STATE::WAKE;
			}
			break;
			
		case STATE::WAKE: // get up
			obj.determineCurAnim(ANIM::CUSTOM[22], 11);
			obj.frameID = int8(obj.var[3] / 7);
			obj.var[3] = obj.var[3] + 1;
			if (obj.var[3] == 13) {
				obj.state = STATE::WALK;
			}
			break;
		
		case STATE::TURN:
			obj.determineCurAnim(ANIM::CUSTOM[22], 6);
			obj.frameID = 0;
			if (obj.var[4] == 7) {
				obj.direction = obj.direction * -1;
				obj.xSpeed = obj.xSpeed * -1;
			}
			if (obj.var[4] == 0) obj.state = STATE::WALK;
			else {
				obj.var[4] = obj.var[4] - 1;
				obj.var[6] = obj.var[6] + 2;
			}
			if (obj.var[6] >= 50) obj.state = STATE::SPRING; // help me stepbro I'm stuck!
			break;
			
		case STATE::SPRING: // a.k.a. start jump
			obj.determineCurAnim(ANIM::CUSTOM[22], 0);
			obj.frameID = int8(obj.var[3] / 7);
			obj.var[3] = obj.var[3] + 1;
			if (obj.var[3] == 41) {
				obj.ySpeed = -5;
				jjSample(obj.xPos, obj.yPos, SOUND::COMMON_JUMP2);
				obj.state = STATE::JUMP;
			}
			break;
			
		case STATE::JUMP:
			if (obj.var[6] > 0) obj.var[6] = obj.var[6] - 1;
			obj.playerHandling = HANDLING::ENEMY;
			obj.bulletHandling = HANDLING::HURTBYBULLET;
			obj.var[3] = 0;
			if (jjGameTicks % 10 == 0) obj.ySpeed += 1;
			obj.xPos += obj.xSpeed;
			obj.yPos += obj.ySpeed;
			if (obj.ySpeed < -0.1) {
				obj.determineCurAnim(ANIM::CUSTOM[22], 1);
				obj.frameID = ((jjGameTicks/7) % jjAnimations[obj.curAnim].frameCount);
			}
			else if (obj.var[4] < 20) { // peak of jump
				obj.determineCurAnim(ANIM::CUSTOM[22], 2);
				obj.frameID = int8(obj.var[4] / 7);
				obj.var[4] = obj.var[4] + 1;
			}
			else {
				obj.determineCurAnim(ANIM::CUSTOM[22], 3);
				obj.frameID = ((jjGameTicks/7) % jjAnimations[obj.curAnim].frameCount);
				}
			if (obj.ySpeed > 0 && jjMaskedPixel(int(obj.xPos + obj.xSpeed), int(obj.yPos + obj.ySpeed) + 18)) { // landing detection
				jjSample(obj.xPos, obj.yPos, SOUND::COMMON_LANDPOP);
				obj.state = STATE::LAND;
			}
			break;
			
		case STATE::LAND:
			obj.var[4] = 0;
			obj.determineCurAnim(ANIM::CUSTOM[22], 4);
			obj.frameID = int8(obj.var[3] / 7);
			obj.var[3] = obj.var[3] + 1;
			if (obj.var[3] == 41) {
				obj.var[1] = 120; // tired from jump
				obj.state = STATE::WALK;
			}
			break;
			
		case STATE::FREEZE:
			if (obj.freeze == 0) obj.state = obj.oldState;
			else obj.freeze--;
			break;
			
		case STATE::DEACTIVATE:
			obj.deactivate();
			break;
		case STATE::KILL:
			obj.delete();
			break;
		}
		
	}
	void onDraw(jjOBJ@ obj) {
		obj.determineCurFrame();
		if (obj.state == STATE::SLEEP) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::PALSHIFT, 32); // I made the bush sprite to be universally usable, but for THESE levels I want it to blend in more
		else obj.draw();
	}
}

enum HeavyCopterVars {
	hcNextPhysicalInjury,
	hcNextNearbyPlayerCheck,
	hcTargetPlayerID,
	hcCurrentAngle
}

class HeavyCopter : jjBEHAVIORINTERFACE {
int lastHitID;
	void onBehave(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::START:
				obj.state = STATE::IDLE;
			case STATE::IDLE: //fall through from START
				obj.yPos = obj.yOrg + jjSin(obj.counterEnd << 2) * 5;
				obj.frameID = (obj.counterEnd) >> 2;
				break;
			case STATE::FLY: {
				const int desiredDistanceFromPlayer = 150;
				const jjPLAYER@ targetPlayer = jjPlayers[obj.var[hcTargetPlayerID]];
				float targetX = targetPlayer.xPos;
				const float targetY = targetPlayer.yPos - 150;
				
				int desiredAngle = 0; //forwards
				int desiredDirection = obj.direction;
				copterChannel = jjSampleLooped(obj.xPos, obj.yPos, SOUND::ORANGE_BUBBELSR, copterChannel);
				if (abs(obj.xPos - targetPlayer.xPos) > desiredDistanceFromPlayer / 2) {
					if (obj.xPos < targetPlayer.xPos) {
						targetX -= desiredDistanceFromPlayer;
						desiredDirection = 1;
					} else {
						targetX += desiredDistanceFromPlayer;
						desiredDirection = -1;
					}
				} else {
					desiredAngle = 17; //down
				}
				
				if (desiredDirection != obj.direction) {
					if (obj.var[hcCurrentAngle] != 17)
						desiredAngle = 17;
					else
						obj.direction = desiredDirection;
				}
				
				if (desiredAngle > obj.var[hcCurrentAngle])
					obj.var[hcCurrentAngle] = obj.var[hcCurrentAngle] + 1;
				else if (desiredAngle < obj.var[hcCurrentAngle])
					obj.var[hcCurrentAngle] = obj.var[hcCurrentAngle] - 1;
				
				if (obj.var[hcCurrentAngle] == 0 || obj.var[hcCurrentAngle] == 17) {
					obj.frameID = (obj.counterEnd) >> 2;
					obj.determineCurAnim(ANIM::CUSTOM[23], obj.var[hcCurrentAngle] == 0 ? 3 : 5);
				} else {
					obj.frameID = (obj.var[hcCurrentAngle] - 1) / 4;
					obj.determineCurAnim(ANIM::CUSTOM[23], 4);
				}
				
				float dx = targetX - obj.xPos;
				float dy = targetY - obj.yPos;
				if (abs(dx) < 7 && abs(dy) < 16) {
					if (desiredDirection == obj.direction && desiredAngle == obj.var[hcCurrentAngle]) {
						obj.state = STATE::FIRE;
						obj.counter = 70;
						obj.counterEnd = 15;
					}
					obj.xSpeed = obj.ySpeed = 0; //only use xSpeed/ySpeed instead of temporary variables because the STATE::FALL code modifies them, just to be cute
				} else {
					int maxSpeed = (jjDifficulty >= 2) ? 3 : 2;
					dx /= 32;
					if (dx > maxSpeed) obj.xSpeed = maxSpeed; 
					else if (dx < -maxSpeed) obj.xSpeed = -maxSpeed;
					else obj.xSpeed = dx;
					obj.xPos += obj.xSpeed;
					
					maxSpeed -= 1; //slower max speed than X
					dy /= 32;
					if (dy > maxSpeed) obj.ySpeed = maxSpeed;
					else if (dy < -maxSpeed) obj.ySpeed = -maxSpeed;
					else obj.ySpeed = dy;
					obj.yPos += obj.ySpeed;
				}
				break; }
			case STATE::FIRE:
				copterChannel = jjSampleLooped(obj.xPos, obj.yPos, SOUND::ORANGE_BUBBELSR, copterChannel);
				if (obj.counterEnd & 15 == 0 && (jjDifficulty != 0 || obj.counterEnd & 31 == 0)) { //the animation is in the firing frame
					jjSample(obj.xPos, obj.yPos, SOUND::COMMON_PISTOL1);
					jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::BLASTERBULLET)];
					bullet.ySpeed = 2 + jjDifficulty;
					bullet.xSpeed = obj.var[hcCurrentAngle] == 0 ? 2 + jjDifficulty : 0;
					bullet.xAcc = 0;
					bullet.playerHandling = HANDLING::ENEMYBULLET;
					bullet.counterEnd = 64;
					bullet.determineCurAnim(ANIM::AMMO, 12);
				}
				obj.frameID = (obj.counterEnd) >> 2;
				if (--obj.counter == 0) //done firing, for now anyway
					obj.state = STATE::FLY;
				break;
			case STATE::FALL:
				copterChannel = jjSampleLooped(obj.xPos, obj.yPos, SOUND::PICKUPS_HELI2, copterChannel);
				if (obj.var[4] == 1) {
					float dirX = (255 * 32) - obj.xPos;
					float dirY = (46 * 32) - obj.yPos;
					float length = sqrt(dirX * dirX + dirY * dirY);
					
					if (length > 0) {
						dirX /= length;
						dirY /= length;
					}
					
					obj.direction = (obj.xPos - (255 * 32) > 0) ? -1 : 1;
					
					obj.xPos += dirX * (2 + obj.xAcc);
					obj.yPos += dirY * (2 + obj.yAcc);
					obj.xAcc = obj.xAcc + 0.2;
					obj.yAcc = obj.yAcc + 0.2;
				}
				else {
					obj.xPos += obj.xSpeed;
					obj.yPos += (obj.ySpeed += 0.08);
				}
				if (jjGameTicks % 20 == 0) {
					jjOBJ@ smoke = jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos)];
					smoke.determineCurAnim(ANIM::AMMO, 72);
				}
				if ((jjMaskedHLine(int(obj.xPos) - 20, 40, int(obj.yPos) + 20) && obj.var[4] == 0) || (obj.var[4] == 1 && obj.yPos >= 45 * 32)) {
					obj.particlePixelExplosion(2);
					jjSample(obj.xPos, obj.yPos, SOUND::COMMON_EXPL_TNT);
					jjOBJ@ boom = jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos)];
					boom.determineCurAnim(ANIM::AMMO, 5);
					int rand = jjRandom() % 4;
					for( int n = 0; n < rand + 3; n++ ) {
						jjOBJ@ shard = jjObjects[jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos)];
						shard.behavior = BEHAVIOR::SHARD;
						shard.determineCurAnim(ANIM::AMMO, (jjRandom()%2 + 78));
					}
					if (obj.var[4] == 1) copterDefeated = true;
					obj.delete();
				} else {
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction);
				}
				return; //skip usual drawing code
			case STATE::FREEZE:
				if (obj.freeze == 0) {
					obj.state = obj.oldState;
					break;
				} else {
					--obj.freeze;
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, mode: SPRITE::FROZEN); //copter
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame - 12, obj.direction, mode: SPRITE::FROZEN); //lizard
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, jjAnimations[obj.curAnim + 3], obj.direction, mode: SPRITE::FROZEN); //gun
					return;
				}
			case STATE::DEACTIVATE:
				if (obj.var[4] == 1) copterDefeated = true; // this can probably be abused but it's the only working solution I can come up with
				obj.deactivate();
				return;
		}
		
		//common code for IDLE, FLY, and FIRE: find a target player, and do drawing
		
		if (jjGameTicks > obj.var[hcNextNearbyPlayerCheck]) {
			const int nearestPlayerID = obj.findNearestPlayer(192 * 192); //same radius as float lizards
			if (nearestPlayerID >= 0 && obj.var[hcTargetPlayerID] != nearestPlayerID) {
				obj.var[hcNextNearbyPlayerCheck] = jjGameTicks + 3 * 70; //next time to start looking for a nearest player
				obj.var[hcTargetPlayerID] = nearestPlayerID;
				if (obj.state == STATE::IDLE)
					obj.state = STATE::FLY;
			}
		}
		if (obj.state == STATE::IDLE && obj.var[4] == 1) obj.yOrg = obj.yOrg + 2;
		++obj.counterEnd;
		obj.determineCurFrame();
		const SPRITE::Mode mode = obj.justHit == 0 ? SPRITE::NORMAL : SPRITE::SINGLECOLOR;
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, mode: mode, param: 15); //copter... the copter is a bigger sprite than the lizard, so it might as well have the collision detection
		if (obj.var[hcCurrentAngle] == 0 || obj.var[hcCurrentAngle] == 17) {
			jjDrawSpriteFromCurFrame( //lizard
				obj.xPos, obj.yPos,
				jjAnimations[obj.curAnim - 3] + ((obj.frameID >> 1) & 3), //slower animation speed (>>3) than the copter (>>2)
				obj.direction, mode: mode, param: 15
			);
			jjDrawSpriteFromCurFrame( //gun
				obj.xPos, obj.yPos,
				obj.state == STATE::FIRE ?
					jjAnimations[obj.curAnim + (obj.var[hcCurrentAngle] == 0 ? 6 : 5)] + (obj.frameID & 3) :
					jjAnimations[obj.curAnim + 3],
				obj.direction, mode: mode, param: 15
			);
		} else {
			jjDrawSpriteFromCurFrame( //lizard
				obj.xPos, obj.yPos,
				jjAnimations[obj.curAnim - 3] + obj.frameID, //when turning, lizard frameID and gun frameID match copter frameID
				obj.direction, mode: mode, param: 15
			);
			jjDrawSpriteFromCurFrame( //gun
				obj.xPos, obj.yPos,
				jjAnimations[obj.curAnim + 3] + obj.frameID,
				obj.direction, mode: mode, param: 15
			);
			
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) { //lots taken from plus52Scripting.j2as
		if (bullet !is null) {
			if ((bullet.var[6] & 16) == 0) { //not a fireball
				bullet.state = STATE::EXPLODE;
			}
			if (obj.freeze != 0) {
				if (force < 3)
					force = 3;
				obj.unfreeze(0);
			}
			hurtObject(obj, force);
		} else {
			if (force == 0) { //collision
				if (obj.state != STATE::FREEZE)
					player.hurt();
			} else {
				if (force == 1) { //sugar rush/buttstomp
					bool bounce = true;
					if (player.buttstomp == 41) //actually stomping
						bounce = !player.hurt(); //stomping but protected by something, including a sugar rush
					if (bounce) {
						player.buttstomp = 50; //landing
						player.ySpeed = player.ySpeed / -2 - 8;
						player.yAcc = 0;
						player.extendInvincibility(-70);
					}
				} else if (force == -101) { //frozen+running
					player.xAcc = 0;
					player.xSpeed /= -2;
					player.ySpeed = -6;
					player.extendInvincibility(-10);
				}
				if (obj.var[hcNextPhysicalInjury] < jjGameTicks) { //hasn't been hurt recently
					obj.var[hcNextPhysicalInjury] = jjGameTicks + 70; //next time this can be hurt by a physical attack
					if (obj.freeze != 0)
						obj.unfreeze(1);
					else
						jjSample(obj.xPos, obj.yPos, ButtstompSounds[jjRandom() % ButtstompSounds.length]);
					hurtObject(obj, 4);
				}
			}
		}
		lastHitID = player.playerID;
		return true;
	}
	void hurtObject(jjOBJ@ obj, int force) const {
		obj.energy -= force;
		if (obj.energy <= 10) { //killed, but with a probably-unnecessary buffer
			givePlayerPointsForObject(jjLocalPlayers[lastHitID], obj);
			obj.state = STATE::FALL;
			obj.playerHandling = HANDLING::PARTICLE;
			jjOBJ@ lizard = jjObjects[jjAddObject(OBJECT::FLOATLIZARD, obj.xPos, obj.yPos, 1,CREATOR::LEVEL, BEHAVIOR::FLOATLIZARD)]; //mostly taken from g_hit.c
			lizard.state = STATE::FALL;
			lizard.xSpeed = obj.direction;
			lizard.ySpeed = 0.5;
			lizard.curAnim = jjAnimSets[ANIM::LIZARD] + 2;
			lizard.counter = 0;
			lizard.frameID = 0; 
			lizard.playerHandling = HANDLING::ENEMY;
			lizard.bulletHandling = HANDLING::HURTBYBULLET;
		} else {
			obj.justHit = 5;
		}
	}
}

class CustomDragonfly : jjBEHAVIORINTERFACE {
	jjOBJ@ rockTarget;
	float triggerDistance = 200;
	float extraStateSpeed = 2;

	CustomDragonfly(jjOBJ@ preset) {
		preset.behavior = this;
	}
	
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::DRAGONFLY, true);
		// 28 - IDLE
		// 8 - START FOLLOW
		// 13 - FOLLOW
		// 22 - ATTACK
		// 30 - RETURN TO SPAWN
		// 3 - DEAD
		
		if (obj.state != STATE::EXTRA) {
			obj.age = 0;
			obj.bulletHandling = HANDLING::HURTBYBULLET;
			obj.playerHandling = HANDLING::ENEMY;
		}
		
		if (obj.state == STATE::FLOAT) {
			for (int i = 0; i < jjObjectCount; i++) { 
				jjOBJ@ rock = jjObjects[i];
				
				if (rock.eventID == 179 && rock.state != STATE::DEACTIVATE) {
					float distance = sqrt(pow(rock.xPos - obj.xPos, 2) + pow(rock.yPos - obj.yPos, 2));
					if (distance <= triggerDistance) {
						@rockTarget = rock;
						obj.state = STATE::EXTRA;
						obj.bulletHandling = HANDLING::IGNOREBULLET;
						obj.playerHandling = HANDLING::PARTICLE;
					}
				}
			}
		}	
		else if (obj.state == STATE::EXTRA) {
			if (rockTarget !is null)
			{
				if (obj.doesCollide(rockTarget)) {
					obj.determineCurAnim(ANIM::CUSTOM[2], 1);
					obj.frameID = (int8(obj.age / 12));
					obj.determineCurFrame();
					
					if (obj.age == 30) jjSample(obj.xPos, obj.yPos, SOUND::DOG_AGRESSIV);
					else if (obj.age == 47) {
						rockTarget.clearPlatform();
						rockTarget.particlePixelExplosion(0);
						rockTarget.state = STATE::DEACTIVATE;
						rockTarget.delete();
						
						
						obj.state = STATE::STOP;
						obj.determineCurAnim(ANIM::DRAGFLY, 0);

					}
					
					if (obj.age > 47) obj.age = 0;
					else obj.age++;
				}
				
				else {
					// Trigonometry. Don't worry about it, I don't understand it either :)
					float dirX = rockTarget.xPos - obj.xPos;
					float dirY = rockTarget.yPos - obj.yPos;
					float length = sqrt(dirX * dirX + dirY * dirY);
					
					if (length > 0) {
						dirX /= length;
						dirY /= length;
					}
					
					obj.direction = (obj.xPos - rockTarget.xPos > 0) ? -1 : 1;
					
					obj.xPos += dirX * extraStateSpeed;
					obj.yPos += dirY * extraStateSpeed;
				}
			}
		}
	}
}

class bigGrub : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) {
	obj.behave(BEHAVIOR::MONITOR);
	obj.beSolid();
	obj.causesRicochet = true;
	obj.bulletHandling = HANDLING::DETECTBULLET;
	obj.playerHandling = HANDLING::SPECIAL;
	obj.scriptedCollisions = true;
	obj.xPos = obj.xOrg;
}
	void onDraw(jjOBJ@ obj) {
		if (obj.state != STATE::FREEZE) obj.frameID = ((jjGameTicks/10) % jjAnimations[obj.curAnim].frameCount);
		obj.determineCurFrame();
		obj.draw();
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (player.doesCollide(obj) && abs(player.xPos - obj.xPos) <= 32.f) player.xPos = player.xPos - (player.xSpeed * 2); // nope.avi
		return true;
	}
}

class grub : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
	jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
	switch(jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,2)) {
		case 0:
			obj.behave(BEHAVIOR::WALKINGENEMY);
			obj.xSpeed = obj.direction * 0.1;
			break;
		case 1:
			switch (obj.state) {
				case STATE::START:
					while (!jjMaskedPixel(int(obj.xPos) - (frame.coldSpotY - frame.hotSpotY) - 4, int(obj.yPos))) obj.xPos = obj.xPos + 1; // fall right
						if (jjRandom()%2 == 0) obj.direction = -1;
						else obj.direction = 1;
						obj.ySpeed = obj.direction * 0.1;
						obj.state = STATE::WALK;
					break; // need to make sure relocation is done
				case STATE::WALK:
					if (
						!jjMaskedHLine(int(obj.xPos) - (frame.coldSpotY - frame.hotSpotY), 8, int(obj.yPos + (obj.ySpeed * (frame.width / 2))))
						||
						jjMaskedHLine(int(obj.xPos - (frame.height / 4)), int(frame.height / 2), int(obj.yPos + (obj.ySpeed * (frame.width / 2))))
						||
						jjEventGet(uint16(obj.xPos / 32), uint16((obj.yPos + obj.ySpeed) / 32)) == 14
					) {
						obj.direction = obj.direction * -1;
						obj.ySpeed = obj.ySpeed * -1;
					}
					else obj.yPos = obj.yPos + obj.ySpeed;
					break;
				case STATE::FREEZE:
					if (obj.freeze == 0) obj.state = obj.oldState;
					else obj.freeze--;
					break;
				case STATE::DEACTIVATE:
					obj.deactivate();
					break;
				case STATE::KILL:
					obj.delete();
					break;
			}
			break;
		case 2:
			switch (obj.state) {
				case STATE::START:
					while (!jjMaskedPixel(int(obj.xPos), int(obj.yPos) + (frame.coldSpotY - frame.hotSpotY) + 4)) obj.yPos = obj.yPos - 1; // fall up
						if (jjRandom()%2 == 0) obj.direction = -1;
						else obj.direction = 1;
						obj.xSpeed = obj.direction * 0.1;
						obj.state = STATE::WALK;
					break; // need to make sure relocation is done
				case STATE::WALK:
					if (
						!jjMaskedVLine(int(obj.xPos + (obj.xSpeed * (frame.width / 2))), int(obj.yPos) + (frame.coldSpotY - frame.hotSpotY) - 7, 10)
						||
						jjMaskedVLine(int(obj.xPos + (obj.xSpeed * (frame.width / 2))), int(obj.yPos - (frame.height / 4)), int(frame.height / 2))
						||
						jjEventGet(uint16(obj.xPos / 32), uint16(obj.yPos / 32)) == 14
					) {
						obj.direction = obj.direction * -1;
						obj.xSpeed = obj.xSpeed * -1;
					}
					else obj.xPos = obj.xPos + obj.xSpeed;
					break;
				case STATE::FREEZE:
					if (obj.freeze == 0) obj.state = obj.oldState;
					else obj.freeze--;
					break;
				case STATE::DEACTIVATE:
					obj.deactivate();
					break;
				case STATE::KILL:
					obj.delete();
					break;
			}
			break;
		case 3:
			switch (obj.state) {
				case STATE::START:
					while (!jjMaskedPixel(int(obj.xPos) + (frame.coldSpotY - frame.hotSpotY) + 4, int(obj.yPos))) obj.xPos = obj.xPos - 1; // fall left
						if (jjRandom()%2 == 0) obj.direction = -1;
						else obj.direction = 1;
						obj.ySpeed = obj.direction * 0.1;
						obj.state = STATE::WALK;
					break; // need to make sure relocation is done
				case STATE::WALK:
					if (
						!jjMaskedHLine(int(obj.xPos) + (frame.coldSpotY - frame.hotSpotY) - 7, 10, int(obj.yPos + (obj.ySpeed * (frame.width / 2))))
						||
						jjMaskedHLine(int(obj.xPos + (frame.height / 4)), int(frame.height / 2), int(obj.yPos + (obj.ySpeed * (frame.width / 2))))
						||
						jjEventGet(uint16(obj.xPos / 32), uint16((obj.yPos + obj.ySpeed) / 32)) == 14
					) {
						obj.direction = obj.direction * -1;
						obj.ySpeed = obj.ySpeed * -1;
					}
					else obj.yPos = obj.yPos + obj.ySpeed;
					break;
				case STATE::FREEZE:
					if (obj.freeze == 0) obj.state = obj.oldState;
					else obj.freeze--;
					break;
				case STATE::DEACTIVATE:
					obj.deactivate();
					break;
				case STATE::KILL:
					obj.delete();
					break;
			}
			break;
		}
	}
	void onDraw(jjOBJ@ obj) {
		
		obj.determineCurFrame();
		switch(jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,2)) {
		case 0:
			obj.draw(); break;
		case 1: 
			if (obj.state == STATE::FREEZE) jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 256, (obj.direction > 0) ? 1 : -1, 1, SPRITE::FROZEN);
			else {
				obj.frameID = ((jjGameTicks/7) % jjAnimations[obj.curAnim].frameCount);
				obj.determineCurFrame();
				if (obj.justHit > 0) jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 256, (obj.direction > 0) ? 1 : -1, 1, SPRITE::SINGLECOLOR, 15);
				else jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 256, (obj.direction > 0) ? -1 : 1, 1);
			}
			break;
		case 2:
			if (obj.state == STATE::FREEZE) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, (obj.direction > 0) ? SPRITE::FLIPV : SPRITE::FLIPHV, SPRITE::FROZEN);
			else {
				obj.frameID = ((jjGameTicks/7) % jjAnimations[obj.curAnim].frameCount);
				obj.determineCurFrame();
				if (obj.justHit > 0) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, (obj.direction > 0) ? SPRITE::FLIPV : SPRITE::FLIPHV, SPRITE::SINGLECOLOR, 15);
				else jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, (obj.direction > 0) ? SPRITE::FLIPV : SPRITE::FLIPHV);
			}
			break;
		case 3: 
			if (obj.state == STATE::FREEZE) jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 768, (obj.direction > 0) ? 1 : 1, -1, SPRITE::FROZEN);
			else {
				obj.frameID = ((jjGameTicks/7) % jjAnimations[obj.curAnim].frameCount);
				obj.determineCurFrame();
				if (obj.justHit > 0) jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 768, (obj.direction > 0) ? 1 : 1, -1, SPRITE::SINGLECOLOR, 15);
				else jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 768, (obj.direction > 0) ? 1 : -1, 1);
			}
			break;
		}
	}
}

void loriBlock(jjOBJ@ obj) {
    obj.behave(BEHAVIOR::DESTRUCTSCENERY);
	int nearPlayerID = obj.findNearestPlayer(256);
    if (obj.state != STATE::KILL && obj.state != STATE::DONE) {
            if (jjLocalPlayers[nearPlayerID].charCurr == CHAR::LORI && jjLocalPlayers[obj.findNearestPlayer(256)].doesCollide(obj) && jjLocalPlayers[nearPlayerID].specialMove > 0) {
                givePlayerPointsForObject(jjLocalPlayers[nearPlayerID], obj);
				obj.state = STATE::KILL;
            }
	}
}

void flickerWrapper(jjOBJ@ obj) {
		if (jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,3) > 3) {
			obj.eventID = 157; 
			obj.behave(BEHAVIOR::FLICKERGEM);
			obj.points = 50;
			obj.var[0] = 0;
			if (jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,3) == 5) obj.determineCurAnim(ANIM::CUSTOM[7], 43);
			else obj.determineCurAnim(ANIM::PICKUPS, 92);
			}
		else {
			obj.behave(BEHAVIOR::FLICKERGEM);
			obj.var[0] = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,3) + 1;
			int points = jjObjectPresets[OBJECT::REDGEM + obj.var[0] - 1].points;
			obj.points = points;
			obj.determineCurAnim(ANIM::PICKUPS, 22);
		}
}

void SuperGemWrapper(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		if (jjParameterGet(uint(obj.xPos) >> 5, uint(obj.yPos) >> 5, 0,3) < 4)
		obj.var[0] = jjParameterGet(uint(obj.xPos) >> 5, uint(obj.yPos) >> 5, 0,3) + 1; //color
		else {
		obj.var[0] = 0;
		obj.age = jjParameterGet(uint(obj.xPos) >> 5, uint(obj.yPos) >> 5, 0,3) - 3;
		}
	}
	obj.behave(BEHAVIOR::SUPERGEM);
	if (obj.state == STATE::SLEEP && obj.var[0] == 0) {
		if (obj.age == 1) obj.determineCurAnim(ANIM::CUSTOM[7], 6);
		if (obj.age == 2) obj.determineCurAnim(ANIM::CUSTOM[7], 42);
	}
	if (obj.state == STATE::ACTION) {
		if (jjParameterGet(uint(obj.xPos) >> 5, uint(obj.yPos) >> 5, 0,3) < 4) jjSample(obj.xOrg, obj.yOrg, SOUND::DOG_SNIF1);
		else jjSample(obj.xOrg, obj.yOrg, SOUND::MONKEY_SPLUT);
	}
}

class parsley : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::PICKUP);
		obj.points = 500;
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@, jjPLAYER@ player, int) {
		increaseMaxHP(player);
		player.invincibility = 70;
		player.blink = 0;
		obj.behavior = BEHAVIOR::EXPLOSION2;
		obj.scriptedCollisions = false;
		obj.frameID = 0;
		return true;
	}
}

void increaseMaxHP(jjPLAYER@ player) {
	int HPmem = player.health; // store HP so it doesn't increase
	jjChat("/smhealth " + (jjMaxHealth + 1));
			
			for (int i = 0; i < 8; ++i) {
				jjAlert("");
			}
	player.health = HPmem;
}

class specialApple : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.eventID = 66; 
		obj.behave(BEHAVIOR::PICKUP);
		obj.points = 50;
		obj.var[0] = 0;
	}
}

class jill : jjBEHAVIORINTERFACE {
    void onBehave(jjOBJ@ obj) {
        obj.putOnGround();
        
        farPlayerID = obj.findNearestPlayer(256 * 32);
        nearPlayerID = obj.findNearestPlayer(256 * 16);
        
        if (obj.state == STATE::START) obj.state = STATE::STILL;

        if (farPlayerID > -1 && obj.state == STATE::DEACTIVATE) obj.state = STATE::STILL;
        
        if (nearPlayerID > -1 && obj.state == STATE::STILL && jjLocalPlayers[nearPlayerID].charCurr != CHAR::FROG) {
			obj.state = STATE::WAKE;
			jillNear = true;
		}
        
        if (obj.state == STATE::WAKE) {
            if (farPlayerID > -1) {
                if (obj.xPos > jjLocalPlayers[farPlayerID].xPos) obj.direction = -1;
				else obj.direction = 1;
			}
            if (jjKey[0x54] && CurrentPopup is null) jillSpeaks(jjLocalPlayers[nearPlayerID]);
            else if (nearPlayerID < 0) {
                obj.state = STATE::STILL;
				jillNear = false;
            }              
        }
    }
	void onDraw(jjOBJ@ obj) {
		obj.frameID = ((jjGameTicks/10) % jjAnimations[obj.curAnim].frameCount);
		obj.determineCurAnim(ANIM::CUSTOM[3], jjParameterGet(int(obj.xOrg / 32), int(obj.yOrg / 32), 0, 2));
		obj.determineCurFrame();
		obj.draw();
		if (obj.state == STATE::WAKE) jjDrawSprite(obj.xPos - 40, obj.yPos - 48, ANIM::CUSTOM[4], 0, (jjGameTicks/12) & 1);
	}
}

void jillSpeaks(jjPLAYER@ play) {
	if (beenBirdie) {
	@CurrentPopup = Conversation(array<Screen@> = {
		Screen(top: Line("thanks, hope you had a good haul!", right: Jill, direction: 1, color: 16)),
		});
	} else {
		if (!jillMet && play.gems[GEM::PURPLE] < 50) {
		@CurrentPopup = Conversation(array<Screen@> = {
			Screen(top: Line("hi, i'm jill! i'm looking for the rare and elusive PURPLE APPLES.", right: Jill, direction: 1, color: 16)),
			Screen(
				top: Line("if you happen to find 50 of those and bring them to me, i'll let you have my BIRDIE FRIEND for a while!", right: Jill, direction: 1, color: 16),
				finish: function(bool skipping) { jillMet = true; } )
		});
		}
		if (!jillMet && play.gems[GEM::PURPLE] >= 50) {
		@CurrentPopup = Conversation(array<Screen@> = {
			Screen(top: Line("hi, i'm jill! i'm looking for the rare and elusive PURPLE APPLES.", right: Jill, direction: 1, color: 16)),
			Screen(top: Line("if you happen to find 50 of those and bring them to me, i'll let you have my BIRDIE FRIEND for a while!", right: Jill, direction: 1, color: 16), choices: array<Choice@> = {
				Choice("give her the apples", effect: function() { jillMet = true; startBirdie(jjLocalPlayers[nearPlayerID]); }),
				Choice("not yet", style: DefaultIfSkippedChoice, effect: function() { jillMet = true; })
			})
		});
		}
		if (jillMet && play.gems[GEM::PURPLE] < 50) {
		@CurrentPopup = Conversation(array<Screen@> = {
			Screen(top: Line("find 50 PURPLE APPLES and bring them to me, and in return i'll let you have my BIRDIE FRIEND for a while!", right: Jill, direction: 1, color: 16)),
			});
		}
		if (jillMet && play.gems[GEM::PURPLE] >= 50) {
		@CurrentPopup = Conversation(array<Screen@> = {
			Screen(top: Line("find 50 PURPLE APPLES and bring them to me, and in return i'll let you have my BIRDIE FRIEND for a while!", right: Jill, direction: 1, color: 16), choices: array<Choice@> = {
				Choice("give her the apples", effect: function() { jillMet = true; startBirdie(jjLocalPlayers[nearPlayerID]); }),
				Choice("not yet", style: DefaultIfSkippedChoice, effect: function() { jillMet = true; })
			}),
		});
		}
	}
}

void startBirdie(jjPLAYER@ play) {
	play.gems[GEM::PURPLE] = play.gems[GEM::PURPLE] - 50;
	jjMusicLoad("CD-Normal.mod");
	play.cameraFreeze((447 * 32), (71 * 32), true, false);
	play.shieldTime = 2800;
	play.shieldType = SHIELD::PLASMA;
	play.warpToID(3, true);
	play.morphTo(CHAR::BIRD, false);
	drawBirdie = false;
	play.cameraUnfreeze(false);
}

void endBirdie(jjPLAYER@ play) {
	beenBirdie = true;
	jjMusicLoad("DD-Jungle.it");
	play.cameraFreeze((441 * 32), (71 * 32), true, false);
	play.revertMorph(false);
	play.warpToID(4, true);
	drawBirdie = true;
	play.cameraUnfreeze(false);
}

void SyncedVine(jjOBJ@ obj) {
	if (jjParameterGet(int(obj.xOrg / 32), int(obj.yOrg / 32), 3, 4) == 0) {
  obj.var[1] = 128; }  //default length in case parameter isn't specifically defined
	else { obj.var[1] = jjParameterGet(int(obj.xOrg / 32), int(obj.yOrg / 32), 3, 4) * 32; }
  
  if (lastSwingingVineLUTLength != obj.var[1]) { //need to generate LUT (LookUp Table) by doing the same math swinging vine objects do
    lastSwingingVineLUTLength = obj.var[1];
    PossibleVineVariableConfigurations = array<array<int>> = {{obj.var[1] * 256, 0}};
    while (true) {
      const array<int>@ oldConfiguration = @PossibleVineVariableConfigurations[PossibleVineVariableConfigurations.length-1];
      array<int> newConfiguration(2);
      newConfiguration[1] = oldConfiguration[1] + ((oldConfiguration[0] > 0) ? -32 : 32);
      newConfiguration[0] = oldConfiguration[0] + newConfiguration[1];
      if (newConfiguration[1] == 0 && newConfiguration[0] == obj.var[1] * 256) //gone full circle
        break;
      PossibleVineVariableConfigurations.insertLast(newConfiguration);
    }
  }
  
  const array<int>@ syncedConfiguration = PossibleVineVariableConfigurations[(jjGameTicks + (jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0, NumberOfBitsDevotedToSyncParameter) * (PossibleVineVariableConfigurations.length / (1 << NumberOfBitsDevotedToSyncParameter)))) % PossibleVineVariableConfigurations.length];
  for (uint i = 0; i < 2; ++i)
    obj.var[2 + i] = syncedConfiguration[i];
    
  //clean up:
  obj.state = STATE::ACTION;
  obj.behavior = BEHAVIOR::SWINGINGVINE;
  obj.behave();
}
int lastSwingingVineLUTLength = -1;
array<array<int>> PossibleVineVariableConfigurations;

class LogSpawner : jjBEHAVIORINTERFACE {
/*    int maxAmount = 4; // of logs
    float timeInSeconds = 8; // delay between each log spawn
replaced these variables with event parameters -GG */
    
    LogSpawner(jjOBJ@ preset) {
        preset.behavior = this;
        preset.bulletHandling = HANDLING::IGNOREBULLET;
        preset.playerHandling = HANDLING::PARTICLE;
    }
    
    void onBehave(jjOBJ@ obj) {
        if (obj.special >= jjParameterGet(int(obj.xOrg / 32), int(obj.yOrg / 32), 0, 4)) return;
        
        if (obj.counter % (jjParameterGet(int(obj.xOrg / 32), int(obj.yOrg / 32), 4, 4) * 50) == 0) {
            jjAddObject(204, obj.xOrg, obj.yOrg);
            obj.special++;
        }
        
        obj.counter++;
    }
}

class Log : jjBEHAVIORINTERFACE {
    float xOld, yOld;
    int width = 70, height = 5; // 160 is the height of the pole sprite. 30 is the width, using that value however leads to some buggy platforms. [added new sprite, disregard height -GG]
    float downSpeed = 0.7, arcSpeed = 3, arcRange = 26;
    // downSpeed - The speed at which the platform goes down after finishing its initial arc.
    // arcSpeed - The speed of the initial arc "animation" (when it goes up and down at the start).
    // arcRange - The range of the arc. Note that higher arcRange values will also raise the platform speed; lower arcSpeed to counteract this effect.
    
    Log(jjOBJ@ preset) {
        preset.behavior = this;
        preset.bulletHandling = HANDLING::IGNOREBULLET;
        xOld = preset.xPos;
        yOld = preset.yPos;
    }
    
    void onBehave(jjOBJ@ obj) {
        int sinValue = int(-obj.counter * arcSpeed);
        int layer = 5; // Draw layer
        
        if (sinValue % 1024 <= -256) // When it's no longer moving up, change layer
            layer = 4;
            
        if (sinValue % 1024 >= -512) { // Before the initial arc is finished, move in sin pattern
            float movement = jjSin(sinValue);
            obj.yPos = obj.yOrg + movement * arcRange;        
            obj.counter++;
        }
        else { // After the initial arc
            obj.yPos += downSpeed;
        }

        int xTile = int(obj.xPos) / 32;
        int yTile = int(obj.yPos) / 32;
        if (jjEventGet(xTile, yTile) == AREA::FLYOFF || yTile > jjLayers[4].height) obj.counter = 0; // Make sure the area event is directly below the pole event [added a failsafe -GG]
        
        jjDrawRotatedSprite(obj.xPos, obj.yPos, ANIM::CUSTOM[6], 1, 0, 255, 1, 1, SPRITE::NORMAL, 0, layer);
        
        xOld = obj.xPos - width + 34;
        yOld = obj.yPos - height;
        
        if (sinValue % 1024 <= -256) // When it's not moving up, make the object a platform
            obj.bePlatform(xOld, yOld, width, height);
			
		if (obj.counter == 0) obj.clearPlatform();
    }
}

class totemTurret : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::POLE);
		if (jjParameterGet(int(obj.xOrg / 32), int(obj.yOrg / 32), 11, 1) == 0) obj.direction = -1;
		else obj.direction = 1;
		obj.frameID = 0;
		obj.determineCurFrame();
		obj.bulletHandling = HANDLING::IGNOREBULLET;
		obj.playerHandling = HANDLING::SPECIAL; // whatever doesn't interact at all
		obj.isBlastable = obj.isFreezable = obj.isTarget = obj.triggersTNT = false;
		if (jjGameTicks % (288 - (jjDifficulty * 48) / jjParameterGet(int(obj.xOrg / 32), int(obj.yOrg / 32), 12, 3)) == 0 && obj.isActive) {
			jjOBJ@ bullet = jjObjects[obj.fireBullet(OBJECT::FIRESHIELDBULLET)];
				bullet.determineCurAnim(ANIM::BUBBA, 4);
				bullet.killAnim = bullet.determineCurAnim(ANIM::AMMO, 3, false);
				bullet.playerHandling = HANDLING::ENEMYBULLET;
				bullet.state = STATE::FLY;
				bullet.xPos = obj.xPos;
				bullet.yPos = obj.yPos; 
				bullet.counterEnd = 63;
				bullet.animSpeed = 1;
		}
		jjPARTICLE@ particle = jjAddParticle(PARTICLE::SMOKE);
		if (particle !is null) {
			particle.xPos = obj.xPos;
			particle.yPos = obj.yPos;
		}
	}
	void onDraw (jjOBJ@ obj) {
	// don't draw
	}

[preview ends here]