Downloads containing CastUltEx2.j2as

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Castle + Damn Ultimate DoubleGJ Tileset conversion N/A Download file

File preview

const bool MLLESetupSuccessful = MLLE::Setup(); ///@MLLE-Generated
#include "MLLE-Include-1.8.asc" ///@MLLE-Generated
#pragma require "CastUltEx2-MLLE-Data-1.j2l" ///@MLLE-Generated
#pragma require "CastUlt-BDay.j2t" ///@MLLE-Generated
#pragma require "DamnUlt-Normal.j2t" ///@MLLE-Generated
#pragma require "CastUltEx2.j2l" ///@MLLE-Generated
#pragma require "CastUlt-BNight.j2t"
#pragma require "CastUlt-Cracco.j2t"
#include "DD-Order.asc"
#include "DD-Rank.asc"
#pragma offer "CastleBreadMix.mp3"
#pragma offer "DD-CastleFast.it"
#pragma offer "DD-BossGBA.it"
#pragma offer "CastleCitizenMix.m4a"
#pragma offer "BEER.it"
#pragma require "DamnUlt-Cave.png"
#pragma require "DamnUlt-CaveUpscaled.png"
#pragma require "aslime2.wav"
#pragma require "chain-rattle1.wav"
#pragma require "chain-rattle2.wav"
#pragma require "squidward-walkL.wav"
#pragma require "squidward-walkR.wav"
#pragma require "blink.wav"
#pragma require "stnloop4.wav"
#pragma require "Stower4b.wav"
#pragma require "Stower51.wav"
#pragma require "bats.wav"
#pragma require "wind30.wav"
#pragma require "Bat_takeoff.ogg"
#pragma require "Bat_loop.ogg"
#pragma require "Bat_idle1.ogg"
#pragma require "Bat_idle2.ogg"
#pragma require "Bat_idle3.ogg"
#pragma require "Bat_idle4.ogg"
#pragma require "floppy-read.wav"
#pragma require "snuff-out1.wav"
#pragma require "snuff-out2.wav"
#pragma require "empty.wav"
#pragma require "common-gemsmsh1.wav"
#pragma require "Dn_battlelord_action_or_roar.wav"
#pragma require "slamD2.wav"
#pragma require "bad-landing.ogg"
#pragma require "falling-whistle-swish.ogg"
#pragma require "sword.ogg"
#pragma require "jjgba-slip.ogg"

const int bestTime = 960;
const float maxScore = 160000;
const int easyTimer = 0;
const int normalTimer = 0;
const int hardTimer = 105000;
const int turboTimer = 84000;

array<uint> fastFeetMode(4, 0);
bool noFast = true;
array<uint8> defaultModTempo = {135, 113}; // index 0 = beer, index 1 = boss
uint8 fadeParam = 255;
string storeMusic = "CastleBreadMix.mp3";
bool computerOn, craccoMode, fadeWall, NPCnear = false;
int farPlayerID, nearPlayerID, batChannel, crate1, crate2, block1, block2 = 0;
int startHP = 5;

array<uint16> spiderTileX(256, 0), spiderTileY(256, 0);
const uint8 CornerEventID = OBJECT::GUN8AMMO3; //whatever

//dragon stuff (see rest in onLevelLoad, onPlayer, and functions from dragonBoss to dragonEdit)
array<bool> dragonSpawned;
array<float> dragonCeiling;
bool dragonReveal, foughtDragon, defeatedDragon = false;

bool inScene = false;
int sceneCounter = 0;
uint8 scenePhase, saberFade, shakeStr = 0;

enum PlayerState { Normal, Loop, Ropeway };

enum LoopAngle { Floor, WallOnLeft, Ceiling, WallOnRight };

class PlayerX {
	PlayerX(){}
	PlayerState State = PlayerState::Normal;
	LoopAngle CurrentAngle = LoopAngle::Floor, NextAngle;
	float CornerXOrg, CornerYOrg, CornerSpeed, CornerElapsed = -1,/* CornerLength,*/ LastX, LastY;
	//int LastLoopEventParameter = -1;
	int LastTile;
	int CornerAngleChange, CornerRepositioningMultiplier, CornerAngleMultiplier;
	int LastHealth = -1;
}

array<PlayerX> PlayerXs(jjLocalPlayerCount);
class CornerEventResult {
	LoopAngle Angle;
	int XOrg, YOrg;
	CornerEventResult(LoopAngle a, int x, int y) { Angle = a; XOrg = x; YOrg = y; }
}
const array<const CornerEventResult@> NextLoopAngles = {
	null, CornerEventResult(WallOnRight, 0, -1), //floor, turn left, loop
	null, CornerEventResult(Floor, 2, 0), //wallonleft, turn left, loop
	null, CornerEventResult(WallOnLeft, 1, 2), //ceiling, turn left, loop
	null, CornerEventResult(Ceiling, -1, 1), //wallonright, turn left, loop

	CornerEventResult(WallOnLeft, 1, -1), null, //floor, turn right, loop
	CornerEventResult(Ceiling, 2, 1), null, //wallonleft, turn right, loop
	CornerEventResult(WallOnRight, 0, 2), null, //ceiling, turn right, loop
	CornerEventResult(Floor, -1, 0), null, //wallonright, turn right, loop

	CornerEventResult(WallOnRight, 1, 3), null, //floor, turn left, corner
	CornerEventResult(Floor, -2, 1), null, //wallonleft, turn left, corner
	CornerEventResult(WallOnLeft, 0, -2), null, //ceiling, turn left, corner
	CornerEventResult(Ceiling, 3, 0), null, //wallonright, turn left, corner

	null, CornerEventResult(WallOnLeft, 0, 3), //floor, turn right, corner
	null, CornerEventResult(Ceiling, -2, 0), //wallonleft, turn right, corner
	null, CornerEventResult(WallOnRight, 1, -2), //ceiling, turn right, corner
	null, CornerEventResult(Floor, 3, 1) //wallonright, turn right, corner
};

void onLevelLoad() {
	initiateDialogue();
	initiateRanking();
	initFade();
	
	jjAnimSets[ANIM::DOG].load(); // for ambient sfx
	jjAnimSets[ANIM::BONUS].load();
	jjAnimSets[ANIM::CUSTOM[14]].load(14, "DD-Anims.j2a");
	jjAnimSets[ANIM::CUSTOM[37]].load(37, "DD-Anims.j2a");
	jjAnimSets[ANIM::ROBOT].load();
	jjAnimSets[ANIM::PLUS_SCENERY].load();
	jjSampleLoad(SOUND::SCIENCE_PLOPKAOS, "stnloop4.wav");
	jjSampleLoad(SOUND::DOG_SNIF1, "Stower4b.wav");
	jjSampleLoad(SOUND::DOG_WAF1, "Stower51.wav");
	jjSampleLoad(SOUND::DOG_WAF2, "bats.wav");
	jjSampleLoad(SOUND::DOG_WAF3, "wind30.wav");
	jjAnimSets[ANIM::ROCK].load(4, "DD-Anims.j2a");
	
///@Event 39=Corner Object |+|Modifier |Corner	
	jjObjectPresets[CornerEventID].behavior = BEHAVIOR::INACTIVE; //zonify
///@Event 40=Loop Object |+|Modifier |Loop	
	jjObjectPresets[OBJECT::GUN9AMMO3].behavior = LoopObject;
	jjObjectPresets[OBJECT::GUN9AMMO3].playerHandling = HANDLING::PARTICLE;
	AddCornerEvents();

///@Event 41=Bomb Demon (armed) |+|Object |Demon |Bomb
///@Event 117=Bomb Demon |-|Enemy |Bomb |Demon |Pinkie:c1
	jjSampleLoad(SOUND::TURTLE_SPK1TURT, "squidward-walkL.wav");
	jjSampleLoad(SOUND::TURTLE_SPK2TURT, "squidward-walkR.wav");
	jjSampleLoad(SOUND::TURTLE_IDLE1, "blink.wav");
	jjAnimSets[ANIM::TURTLE].load(29, "DD-Anims.j2a");
	jjObjectPresets[OBJECT::NORMTURTLE].behavior = bombDemon();
	jjObjectPresets[OBJECT::NORMTURTLE].scriptedCollisions = true;
	jjObjectPresets[OBJECT::NORMTURTLE].playerHandling = HANDLING::SPECIAL;
	demonBomb(jjObjectPresets[OBJECT::TURTLESHELL]);
	jjPlayers[31].furSet(88, 48, 32, 72);
	jjObjectPresets[OBJECT::FIVEHUNDREDBUMP].behavior = bumpFix();
	jjObjectPresets[OBJECT::FIVEHUNDREDBUMP].scriptedCollisions = true;

///@Event 186=Destruct Scenery (Bomb) |+|Trigger   |Bomb   |Scen   |Special:2	
	jjObjectPresets[OBJECT::DESTRUCTSCENERYBOMB].behavior = bombScenEdit;
	jjObjectPresets[OBJECT::BOMB].behavior = bombEdit;

///@Event 120=Normal Demon |-|Enemy |Norm |Demon |Pinkie:c1
	jjObjectPresets[OBJECT::DEMON].behavior = demonEdit();

///@Event 125=Tuff Demon |-|Enemy |Tuff |Demon
	jjAnimSets[ANIM::CUSTOM[27]].load(27, "DD-Anims.j2a");
	jjObjectPresets[OBJECT::FATCHICK].energy = 5;
	jjObjectPresets[OBJECT::FATCHICK].points = 500;
	jjObjectPresets[OBJECT::FATCHICK].behavior = tuffDemon();

///@Event 237=Pentagram Portal |-|Enemy |Penta|-gram |Spawn:{Bomb Demon,Pink Bomb Demon,Tuff Demon,Normal Demon,Pink Demon}3 |RandomDir:c1
	jjAnimSets[ANIM::CUSTOM[30]].load(30, "DD-Anims.j2a");
	jjSampleLoad(SOUND::INTRO_HITSPAZ, "snuff-out1.wav");
	jjSampleLoad(SOUND::INTRO_HITTURT, "snuff-out2.wav");
	pentagram(jjObjectPresets[OBJECT::BEEBOY]);
	
///@Event 127=Imp |-|Enemy|Imp| |X Range:3|Y Range:3
	jjAnimSets[ANIM::CUSTOM[26]].load(26, "DD-Anims.j2a");
	jjObjectPresets[OBJECT::FISH].behavior = imp();	
	jjObjectPresets[OBJECT::FISH].determineCurAnim(ANIM::CUSTOM[26], 0);
	jjObjectPresets[OBJECT::FISH].points = 50;

///@Event 96=Decorative Vine|+|Scenery|Decor|Vine|Length:3|Foreground:c1|XOffset:3|YOffset:3
	jjSampleLoad(SOUND::INTRO_UP1, "chain-rattle1.wav");
	jjSampleLoad(SOUND::INTRO_UP2, "chain-rattle2.wav");
	const uint vineFrameID = jjAnimations[jjAnimSets[ANIM::CUSTOM[13]].allocate(array<uint> = {8})];
	for (uint i = 0; i < 8; ++i) {
		jjANIMFRAME@ vineFrame = jjAnimFrames[vineFrameID + i];
		jjPIXELMAP vineImage(i * 32 + 8, 5*32, 16, 52 + i*16, 4);
		for (uint x = 0; x < vineImage.width; ++x)
			for (uint y = 0; y < vineImage.height; ++y)
				if (vineImage[x,y] == 0)
					vineImage[x,y] = 128;
		vineImage.save(vineFrame);
		vineFrame.hotSpotX = -8;
	}
	jjOBJ@ preset = jjObjectPresets[96];
	preset.behavior = DecorativeVine;
	preset.curFrame = vineFrameID;
	preset.playerHandling = HANDLING::PARTICLE;
	
///@Event 126=Lava Block |+|Scenery |Lava |Block |Adjust Y:5 |Deep:c1
	jjAnimSets[ANIM::CUSTOM[9]].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 111=Dripper |+|Scenery |Drip | |Speed:2 |Lava:c1
	jjAnimSets[ANIM::CUSTOM[28]].load(28, "DD-Anims.j2a");
	jjObjectPresets[OBJECT::CHESHIRE1].behavior = dripper();

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

///@Event 38=Little Bat |+|Scenery |Batty |Batt
	jjObjectPresets[OBJECT::TNTAMMO3].behavior = littleBat();
	jjObjectPresets[OBJECT::TNTAMMO3].playerHandling = HANDLING::PARTICLE;
	jjSampleLoad(SOUND::P2_THROW, "Bat_takeoff.ogg");
	jjSampleLoad(SOUND::P2_FART, "Bat_loop.ogg");
	jjSampleLoad(SOUND::P2_FOEW1, "Bat_idle1.ogg");
	jjSampleLoad(SOUND::P2_FOEW4, "Bat_idle2.ogg");
	jjSampleLoad(SOUND::P2_FOEW5, "Bat_idle3.ogg");
	jjSampleLoad(SOUND::P2_POEP, "Bat_idle4.ogg");
	
	jjAnimSets[ANIM::CUSTOM[3]].load(3, "DD-Anims.j2a"); // DD pickups
	jjAnimSets[ANIM::CUSTOM[5]].load(5, "DD-Anims.j2a");
	jjAnimSets[ANIM::CUSTOM[11]].load(11, "DD-Anims.j2a");
	jjAnimSets[ANIM::CUSTOM[12]].load(12, "DD-Anims.j2a");
	jjSampleLoad(SOUND::ENDING_OHTHANK, "floppy-read.wav");
	jjObjectPresets[OBJECT::PURPLEGEM].behavior = floppy();
	jjObjectPresets[OBJECT::PURPLEGEM].determineCurAnim(ANIM::CUSTOM[11], 0);
///@Event 184=Computer |+|Object |PC
	jjObjectPresets[OBJECT::STANDMONKEY].behavior = computer();
	jjObjectPresets[OBJECT::STANDMONKEY].determineCurAnim(ANIM::CUSTOM[11], 1);
	jjObjectPresets[OBJECT::STANDMONKEY].bulletHandling = HANDLING::IGNOREBULLET;
	jjObjectPresets[OBJECT::STANDMONKEY].playerHandling = HANDLING::SPECIAL;
///@Event 141=Peppermint |+|Food |Pepr-|mint
	jjObjectPresets[OBJECT::APPLE].determineCurAnim(ANIM::CUSTOM[3], 55);
///@Event 145=Caramel |+|Food |Cara |-mel
	jjObjectPresets[OBJECT::PEAR].determineCurAnim(ANIM::CUSTOM[3], 56);
///@Event 154=Parsley Max Energy +1 |+|Goodies |Parsley	
	jjObjectPresets[OBJECT::LEMON].determineCurAnim(ANIM::CUSTOM[3], 2);
	jjObjectPresets[OBJECT::LEMON].scriptedCollisions = true;
	jjObjectPresets[OBJECT::LEMON].behavior = parsley();
	startHP = jjMaxHealth;
///@Event 159=Cookie|+|Food |Cookie
	jjObjectPresets[OBJECT::GRAPES].determineCurAnim(ANIM::CUSTOM[3], 0);
///@Event 160=Bread (white)|+|Food |Bread
	jjObjectPresets[OBJECT::LETTUCE].determineCurAnim(ANIM::CUSTOM[3], 68);
///@Event 156=Bread (wholegrain)|+|Food |Whole |Bread
	jjObjectPresets[OBJECT::THING].determineCurAnim(ANIM::CUSTOM[3], 69);
///@Event 157=Bun |+|Food |Bun
	jjObjectPresets[OBJECT::WATERMELON].determineCurAnim(ANIM::CUSTOM[3], 70);
///@Event 161=Kaiser Roll |+|Food |Kaiser
	jjObjectPresets[OBJECT::EGGPLANT].determineCurAnim(ANIM::CUSTOM[3], 71);
///@Event 162=Croissant |+|Food |Croi- |ssant
	jjObjectPresets[OBJECT::CUCUMBER].determineCurAnim(ANIM::CUSTOM[3], 72);
///@Event 158=Swiss Roll |+|Food |Swiss |Roll
	jjObjectPresets[OBJECT::PEACH].determineCurAnim(ANIM::CUSTOM[3], 51);
///@Event 142=Canned Beans |+|Food |Can |Bean
	jjObjectPresets[OBJECT::BANANA].determineCurAnim(ANIM::CUSTOM[3], 74);
///@Event 143=Eclair |+|Food |Eclair
	jjObjectPresets[OBJECT::CHERRY].determineCurAnim(ANIM::CUSTOM[3], 76);
///@Event 144=Apple Pie |+|Food |Apple |Pie
	jjObjectPresets[OBJECT::ORANGE].determineCurAnim(ANIM::CUSTOM[3], 52);
///@Event 147=Toast |+|Food |Toast
	jjObjectPresets[OBJECT::STRAWBERRY].determineCurAnim(ANIM::CUSTOM[3], 75);
///@Event 155=Muffin |+|Food |Muffin
	jjObjectPresets[OBJECT::LIME].determineCurAnim(ANIM::CUSTOM[3], 77);
///@Event 170=White Chocolate |+|Food |White |Choc
	jjObjectPresets[OBJECT::CHIPS].determineCurAnim(ANIM::CUSTOM[3], 48);
///@Event 176=Fudge |+|Food |Fudge
	jjObjectPresets[OBJECT::FRIES].determineCurAnim(ANIM::CUSTOM[3], 62);
///@Event 164=Beer |+|Food |Beer
	jjObjectPresets[OBJECT::PEPSI].determineCurAnim(ANIM::CUSTOM[3], 78);
///@Event 67=Super Item |+|Goodies |Super  |Item|Type:{Red Gem,Green Gem,Blue Gem,Purple Gem,Watermelon,Jackfruit,Cheese Wheel}3
	jjObjectPresets[OBJECT::SUPERGEM].behavior = SuperGemWrapper;
	jjObjectPresets[OBJECT::FLICKERGEM].behavior = flickerWrapper;
	jjSampleLoad(SOUND::COMMON_GEMSMSH1, "empty.wav");
	jjSampleLoad(SOUND::INTRO_GRAB, "common-gemsmsh1.wav");
	
	jjObjectPresets[OBJECT::FASTFEET].points = 100;
	jjObjectPresets[OBJECT::FASTFEET].scriptedCollisions = true;
	jjObjectPresets[OBJECT::FASTFEET].behavior = FastFeet();

///@Event 107=Spider |-|Enemy |Spider | |Web:{None,Center,Left,Right}2
	jjAnimSets[ANIM::VINE].load();
	jjAnimSets[ANIM::CUSTOM[31]].load(31, "DD-Anims.j2a");
	jjObjectPresets[OBJECT::SPARK].behavior = function(obj) {
		obj.behavior = spider();
		obj.behave();
	};
///@Event 42=Swinging Web |+|Object    |Swing  |Web
	jjObjectPresets[OBJECT::SWINGINGVINE].behavior = swingingWeb();
	jjPlayers[30].furSet(64, 64, 64, 64); // lol fuck it

///@Event 151=Dragon Boss |-|Boss |Dragon |Boss |Rotate:c1 |DrawBody:c1
	jjAnimSets[ANIM::CUSTOM[33]].load(33, "DD-Anims.j2a"); // dragon boss sprites
	jjAnimSets[ANIM::CUSTOM[34]].load(34, "DD-Anims.j2a"); // BL18 egg (for dragon boss)
	jjSampleLoad(SOUND::INTRO_MONSTER2, "Dn_battlelord_action_or_roar.wav");
	jjSampleLoad(SOUND::INTRO_HITSPAZ, "slamD2.wav");
	jjAnimSets[ANIM::DEVILDEVAN].load(); // need this for one damn sample...
	dragonBoss(jjObjectPresets[OBJECT::QUEEN]);
///@Event 201=
///@Event 202=
///@Event 225=
///@Event 241=
	dragonArm(jjObjectPresets[OBJECT::ROBOT]);
	jjObjectPresets[OBJECT::DEVANROBOT].behavior = dragonWings();
	jjObjectPresets[OBJECT::TWEEDLEBOSS].behavior = dragonTail();
	jjObjectPresets[OBJECT::TWEEDLEBOSS].playerHandling = HANDLING::PARTICLE;
	jjObjectPresets[OBJECT::TWEEDLEBOSS].bulletHandling = HANDLING::IGNOREBULLET;
	jjObjectPresets[OBJECT::BEES].behavior = egg();
	jjObjectPresets[OBJECT::DRAGON].behavior = dragonEdit;
	jjObjectPresets[OBJECT::BAT].behavior = batEdit; // not necessary for your level possibly

///@Event 226=Ropeway |+|Object |Rope |Way |Direction:{Left,Right}1|Spins:c1	
	jjAnimSets[ANIM::CUSTOM[32]].load(32, "DD-Anims.j2a");
	jjObjectPresets[OBJECT::COPTER].behavior = Ropeway();
	jjObjectPresets[OBJECT::COPTER].scriptedCollisions = true;
	jjObjectPresets[OBJECT::COPTER].curFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[32]]];
	
///@Event 217=NPC |+|Object |NPC | |Type:{Jill,Yoman,Chief,Savage (male),Savage (female),Guard}3 |Order ID:5
	jjAnimSets[ANIM::CUSTOM[16]].load(16, "DD-Anims.j2a"); // NPC sprites
	jjAnimSets[ANIM::CUSTOM[5]].load(5, "DD-Anims.j2a"); // dialogue prompt sprite
	jjObjectPresets[OBJECT::EVA].behavior = NPC();

///@Event 134=
///@Event 135=
	jjSampleLoad(SOUND::INTRO_SKI, "falling-whistle-swish.ogg");
	jjSampleLoad(SOUND::INTRO_INSECT, "bad-landing.ogg");
	jjSampleLoad(SOUND::INTRO_KATROL, "jjgba-slip.ogg");
	jjSampleLoad(SOUND::INTRO_SWISH1, "sword.ogg");
	jjAnimSets[ANIM::CUSTOM[35]].load(35, "DD-Anims.j2a");
	jjAnimSets[ANIM::CUSTOM[36]].load(36, "DD-Anims.j2a");
	jjObjectPresets[OBJECT::SEEKERPOWERUP].behavior = admael;
	jjObjectPresets[OBJECT::SEEKERPOWERUP].bulletHandling = HANDLING::IGNOREBULLET;
	jjObjectPresets[OBJECT::SEEKERPOWERUP].playerHandling = HANDLING::PARTICLE;
	jjObjectPresets[OBJECT::RFPOWERUP].behavior = eva;
	jjObjectPresets[OBJECT::RFPOWERUP].bulletHandling = HANDLING::IGNOREBULLET;
	jjObjectPresets[OBJECT::RFPOWERUP].playerHandling = HANDLING::PARTICLE;
	
///@Event 244=Falling Brick |+|Object |Brick |Trap |BGWall:c1
	jjObjectPresets[OBJECT::CTFBASE].behavior = brickTrap();

///@Event 129=Takeoff |+|Modifier |Take	|Off |Left:c1
	jjObjectPresets[OBJECT::STEAM].behavior = BEHAVIOR::INACTIVE; //zonify
	
///@Event 254=Delet This |-|Modifier |NOT |v
    deleteUnwantedEvents();
	if (jjDifficulty > 0) jjObjectPresets[OBJECT::SPIKEBOLL].bulletHandling = HANDLING::IGNOREBULLET;
	
	jjAnimSets[ANIM::HELMUT].load(); // for pixelmaps
	// cavern pixelmap:
	jjPIXELMAP cavernup("DamnUlt-CaveUpscaled.png");
	jjANIMFRAME@ helmut1 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim].firstFrame];
	cavernup.save(helmut1);
	helmut1.hotSpotX = -helmut1.width/2;
	helmut1.hotSpotY = -helmut1.height/2;
	jjPIXELMAP cavern("DamnUlt-Cave.png");
	jjANIMFRAME@ helmut2 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim].firstFrame + 1];
	cavern.save(helmut2);
	helmut2.hotSpotX = -helmut2.width/2;
	helmut2.hotSpotY = -helmut2.height/2;
	// bones pixelmap:
	jjPIXELMAP bones("DamnUlt-Bones.png");
	jjANIMFRAME@ helmut3 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim].firstFrame + 2];
	bones.save(helmut3);
	helmut3.hotSpotX = 0;
	helmut3.hotSpotY = 0;
	
	jjLAYER@ coverLayer = jjLayers[3];
	coverLayer.generateSettableTileArea(607, 2, 20, 7);
	
	jjLayerHasTiles[6] = jjLayerHasTiles[7] = false;
}

void onLevelBegin() {
	setTimer();
	int i = 0;
	for (int xTile = jjLayerWidth[4]; --xTile >= 0;) {
        for (int yTile = jjLayerHeight[4]; --yTile >= 0;) {
            if (jjEventGet(xTile,yTile) == OBJECT::SPARK && jjParameterGet(xTile, yTile, 0, 2) != 0) {
				spiderTileX[i] = xTile;
				spiderTileY[i] = yTile;
				i++;
			}
		}
	}
}

void onLevelReload() {
	scoreSeconds = storeScoreSeconds;
	if (!jjLayers[2].hasTiles) jjTriggers[4] = true;
	if (craccoMode) {
		jjPAL craccoPal;
		craccoPal.load("CastUlt-Cracco.j2t");
		apply(craccoPal);
	}
	if (storeMusic == "DD-BossGBA.it") {
		if (craccoMode) storeMusic = "BEER.it";
		else storeMusic = "CastleBreadMix.mp3";
	}
}

void onMain() {
	if (jjDifficulty == 0) oneUpsIntoBlueGems();
	if ((jjGameTicks % 70) == 0 && !levelOver && CurrentPopup is null) scoreSeconds++;	
	rankingSetup();
	if (levelOver) updateFade();
	if (fadeWall) {
		jjLayers[2].spriteMode = SPRITE::BLEND_DISSOLVE;
		jjLayers[2].spriteParam = fadeParam;
		if (fadeParam > 0) fadeParam--;
		else jjLayers[2].hasTiles = false; // clean up mem
	}
	if (inScene) {
		if (sceneCounter == 240 && fadeWall) {
			continueTalk(jjLocalPlayers[0], 0);
			inScene = false;
		}
		if (defeatedDragon && CurrentPopup is null) {
			switch (sceneCounter) {
			case 0:
				jjSamplePriority(SOUND::INTRO_SKI);
				break;
			case 50:
				jjSample(jjLocalPlayers[0].xPos, jjLocalPlayers[0].yPos, SOUND::COMMON_BLUB1);
				break;
			case 120:
				dropGirls();
				break;
			case 290:
				jjMusicResume();
				storeMusic = "CastleCitizenMix.m4a";
				inScene = false;
				continueTalk(jjLocalPlayers[0], 2);
				break;
			case 330:
				scenePhase = 2;
				break;
			case 400:
				inScene = false;
				continueTalk(jjLocalPlayers[0], 3);
				break;
			}
		}
		sceneCounter++;
	}
	
	for (int i = 0; i <= jjLocalPlayerCount; i++) {
		if (fastFeetMode[i] != 0) {
			noFast = false;
			break;
		} else noFast = true;
	}
	if (noFast && !levelOver) {
		jjMusicLoad(storeMusic);
		if ((storeMusic == "BEER.it") && uint8(jjGetModTempo()) != defaultModTempo[0]) jjSetModTempo(defaultModTempo[0]);
		else if (storeMusic == "DD-BossGBA.it" && uint8(jjGetModTempo()) != defaultModTempo[1]) jjSetModTempo(defaultModTempo[1]);
	}
}

void onPlayer(jjPLAYER@ play) {
	dialogueInterface(play);
	rankingInterface(play);
	if (inScene) {
		play.timerPause();
		play.invincibility = -15;
		play.keyLeft = play.keyRight = play.keyDown = play.keyUp = play.keyRun = play.keyFire = play.keyJump = play.keySelect = false;
		play.idle = 0;
		if (defeatedDragon && CurrentPopup is null) {
			if (sceneCounter > 50 && sceneCounter <= 150) jjDrawSprite(play.xPos + 40, play.yPos - 40, ANIM::CUSTOM[5], 1, 1, 0, SPRITE::NORMAL, 0, 3);
			if (sceneCounter == 140) {
				if (play.xPos >= 691 * 32) play.direction = -1;
				else play.direction = 1;
			}
		}
	}
	if (!levelOver) {
	if (fastFeetMode[play.playerID] > 0) {
//		if (playX.State != PlayerState::Loop) {
			if (play.xAcc > 0) {
				play.xAcc = play.xAcc + 1;
				play.xSpeed = play.xSpeed + 2;
			}
			if (play.xAcc < 0) {
				play.xAcc = play.xAcc - 1;
				play.xSpeed = play.xSpeed - 2;
			}
			play.keyRun = true;
//		}
		fastFeetMode[play.playerID]--;
		if (play.xPos > 232 * 32 && play.xPos < 239 * 32 && play.yPos > 24 * 32 && play.yPos < 34 * 32 && play.xSpeed > 16.0) {
			for (int x = int(play.xPos) + 16; x < play.xPos + play.xSpeed; ++x) {
				if (jjMaskedVLine(x, int(play.yPos) - 24, 48)) {
				play.xSpeed = 16.0;
				break;
				}
			}
		}
	}

// DRAGON BOSS STUFF
	int maxBossDistance = 567; // push distance; adjust for your boss arena (this whole part is to prevent getting stuck in a wall)
	if (jjGameTicks > 10) { // maybe let's let stuff initiate fully first...
		for (int i = 1; i < jjObjectCount; i++) {
			jjOBJ@ other = jjObjects[i];
			if (other.eventID == 151) {
				if (other.direction == -1) {
					if (play.bossActivated && other.state == STATE::PUSH && !jjMaskedPixel(int(play.xPos + play.xSpeed - 4), int(play.yPos))) play.xPos = play.xPos - 2;
					if (play.bossActivated && play.xPos < other.xPos - maxBossDistance) play.xPos = other.xPos - maxBossDistance;
					if (other.counterEnd != 3 && play.xPos > other.xPos - 24) play.xPos = other.xPos - 24;
				} else {
					if (play.bossActivated && other.state == STATE::PUSH && !jjMaskedPixel(int(play.xPos + play.xSpeed + 4), int(play.yPos))) play.xPos = play.xPos + 2;
					if (play.bossActivated && play.xPos > other.xPos + maxBossDistance) play.xPos = other.xPos + maxBossDistance;
					if (other.counterEnd != 3 && play.xPos < other.xPos + 24) play.xPos = other.xPos + 24;
				}
			}
		}	
	}
	
	PlayerX@ playX = PlayerXs[play.localPlayerID];
	if (playX.LastHealth != play.health) {
		if (playX.LastHealth > play.health) { //injured
			playX.State = PlayerState::Normal;
			playX.CurrentAngle = LoopAngle::Floor;
			playX.LastTile = -1;
			play.invisibility = false;
			play.cameraUnfreeze();
		}
		playX.LastHealth = play.health;
		return;
	}
	
	if (playX.State == PlayerState::Ropeway) {
		play.keyLeft = play.keyRight = play.keyRun = play.keyDown = false;
		play.xSpeed = play.ySpeed = 0;
		//play.invincibility = -2;
		return;
	}
	
	const uint xTile = uint(play.xPos) >> 5, yTile = uint(play.yPos) >> 5;
	const uint8 currEvent = jjEventGet(xTile,yTile);
	if (currEvent == CornerEventID) {
		const int loopEventParameter = jjParameterGet(xTile, yTile, 0, 4);
		if ((loopEventParameter >> 2) == playX.CurrentAngle) {
			const auto curAnim = play.curAnim - jjAnimSets[play.setID];
			if (play.currTile != playX.LastTile && (playX.State == PlayerState::Loop || (curAnim >= RABBIT::RUN1 && curAnim <= RABBIT::RUN3))) { //running
				//if (loopEventParameter != playX.LastLoopEventParameter) {
					const int circumstance = ((loopEventParameter & 3) << 3) | (playX.CurrentAngle << 1) | (play.direction == 1 ? 1 : 0);
					const CornerEventResult@ result = NextLoopAngles[circumstance];
					if (result !is null) {
						playX.LastTile = play.currTile;
						//playX.LastLoopEventParameter = loopEventParameter;
						//jjAlert(result.Angle + "," + result.XOrg + "," + result.YOrg);
						playX.NextAngle = result.Angle;
						playX.CornerXOrg = (int(xTile) + result.XOrg) * 32;
						playX.CornerYOrg = (int(yTile) + result.YOrg) * 32;
						const bool isLoop = loopEventParameter & 2 == 0;
						//playX.CornerLength = isLoop ? 256 : 256; //todo
						playX.CornerElapsed = 0; //todo?
						playX.CornerRepositioningMultiplier = isLoop ? 50 : 84; //40:120
						playX.CornerAngleMultiplier = isLoop ? 1 : -1;
						playX.CornerSpeed = isLoop ? 30 : 18;
						playX.CornerAngleChange = int(playX.NextAngle - playX.CurrentAngle);
						if (abs(playX.CornerAngleChange) == 3)
							playX.CornerAngleChange /= -3;
						playX.State = PlayerState::Loop;
						play.invisibility = true;
						//play.noclipMode = true;
						play.xSpeed = play.ySpeed = 0;
						if (jjParameterGet(xTile, yTile, -2, 1) == 1)
							play.cameraFreeze(play.cameraX, play.cameraY, false, true);
					}
				//}
			}
		}
	} else if (currEvent == AREA::FLYOFF) { //this feels like the sensible choice
		if (playX.State == PlayerState::Loop) play.keyJump = true;
		play.ballTime = 0;
	} else if (currEvent == AREA::PATH) { // not renaming the event and its parameters cuz the original functionality is retained, this is just an add-on; refer to param values below
		float entryX;
		if (abs(play.xSpeed) >= 16.0) entryX = play.xSpeed;
		else entryX = play.xSpeed * 1.05;
		if (play.currTile != playX.LastTile && jjParameterGet(xTile, yTile, 9, 1) == 0) { // visited tile for the first time AND is entry/exit point?
			play.ballTime = 120;
			play.xPos = xTile * 32 + 16;
			play.yPos = yTile * 32 + 16;
			playX.LastTile = play.currTile;
			play.xSpeed = entryX;
			switch (jjParameterGet(xTile, yTile, 6, 3)) { // entry/exit type
				case 0: play.ySpeed = entryX / 2.1; break; // left edge spiral top
				case 1: play.ySpeed = entryX / -2.1; break; // right edge spiral top
				case 2: if (entryX < 0) play.ySpeed = entryX / 2; else play.ySpeed = 0; break; // left edge spiral bottom
				case 3: if (entryX > 0) play.ySpeed = entryX / -2; else play.ySpeed = 0; break; // right edge spiral bottom
				case 4: if (entryX < 0) play.ySpeed = entryX / 2; else play.ySpeed = entryX; break; // left edge spiral bottom into slope
				case 5: if (entryX > 0) play.ySpeed = entryX / -2; else play.ySpeed = entryX * -1; break; // right edge spiral bottom into slope
			}
			play.noclipMode = !play.noclipMode; // if already noclip then not noclip, else do noclip.
		} else if (play.currTile != playX.LastTile && jjParameterGet(xTile, yTile, 9, 1) == 1 && play.noclipMode) { // inner spiral node, must be noclip
			play.xPos = xTile * 32 + 16;
			play.yPos = yTile * 32 + 16;
			playX.LastTile = play.currTile;
			play.xSpeed = entryX * -1; // bounce to the other side
			play.ySpeed = play.ySpeed * 1.05; // adjust for increasing xSpeed
		}
	} else if (currEvent == OBJECT::STEAM) {
		if (play.currTile != playX.LastTile && ((play.xSpeed >= 4.0 && jjParameterGet(xTile, yTile, 0, 1) == 0) || (play.xSpeed <= -4.0 && jjParameterGet(xTile, yTile, 0, 1) == 1))) {
			if (play.xSpeed > 16.0) play.xSpeed = 16.0;
			else if (play.xSpeed < -16.0) play.xSpeed = -16.0;
			play.spring(0, abs(play.xSpeed) * -1, true, false);
			playX.LastTile = play.currTile;
		}
	} else {
		playX.LastTile = -1;
		if (!inScene && CurrentPopup is null && !levelOver) {
			if (shakeStr > 0) {
				if (jjGameTicks % 3 == 0) {
					if ((jjGameTicks % 6) == 0) play.cameraFreeze(play.xPos, play.yPos - shakeStr, true, false);
					else play.cameraFreeze(play.xPos, play.yPos + shakeStr, true, false);
				}
			shakeStr -= uint(jjSubscreenHeight / 150);
			} else play.cameraUnfreeze();
		}
	}
	
	int angle = playX.CurrentAngle << 8;
	const int speedDivider = play.keyRun ? 1 : 4;
	if (playX.CornerElapsed >= 0) {
		playX.CornerElapsed += playX.CornerSpeed / speedDivider;
		if (playX.CornerElapsed > 256)
			playX.CornerElapsed = 256;
		angle += int(playX.CornerElapsed * playX.CornerAngleChange);
		//play.xPos = playX.LastX = playX.CornerXOrg + jjSin(angle) * playX.CornerRepositioningMultiplier * -playX.CornerAngleMultiplier;
		//play.yPos = playX.LastY = playX.CornerYOrg - jjCos(angle) * playX.CornerRepositioningMultiplier * -playX.CornerAngleMultiplier;
		playX.LastX = playX.CornerXOrg + jjSin(angle) * playX.CornerRepositioningMultiplier * -playX.CornerAngleMultiplier;
		playX.LastY = playX.CornerYOrg - jjCos(angle) * playX.CornerRepositioningMultiplier * -playX.CornerAngleMultiplier;
		play.xSpeed = playX.LastX - play.xPos;
		play.ySpeed = playX.LastY - play.yPos;
		if (playX.CornerElapsed == 256) {
			playX.CornerElapsed = -1;
			playX.CurrentAngle = playX.NextAngle;
		}
	} else if (playX.State == PlayerState::Loop) {
		const int speedMultiplier = 16 / speedDivider * play.direction;
		//play.xPos = playX.LastX += jjCos(angle) * speedMultiplier;
		//play.yPos = playX.LastY += jjSin(angle) * speedMultiplier;
		playX.LastX += jjCos(angle) * speedMultiplier;
		playX.LastY += jjSin(angle) * speedMultiplier;
		play.xSpeed = playX.LastX - play.xPos;
		play.ySpeed = playX.LastY - play.yPos;
	}
	
	if (playX.State == PlayerState::Loop) {
		if (playX.CornerAngleMultiplier == 1 && playX.CornerElapsed >= 0) //quarter pipe
			play.keyJump = false;
		if ((playX.CornerElapsed < 0 && playX.CurrentAngle == LoopAngle::Floor) || play.keyJump || (play.direction == 1 && !play.keyRight) || (play.direction == -1 && !play.keyLeft) || jjMaskedPixel(int(playX.LastX), int(playX.LastY))) {
			playX.State = PlayerState::Normal;
			playX.CurrentAngle = LoopAngle::Floor;
			playX.CornerElapsed = -1;
			play.invisibility = false;
			play.xSpeed = play.direction * jjCos(angle) * 16 / speedDivider;
			play.ySpeed = play.direction * jjSin(angle) * 16 / speedDivider;
			play.cameraUnfreeze();
		} else {
			play.keyLeft = play.keyRight = play.keyFire = false;
			jjDrawRotatedSprite(play.xPos, play.yPos, play.setID, play.keyRun ? RABBIT::RUN3 : RABBIT::RUN1, jjGameTicks >> 2, -angle, play.direction,1, SPRITE::PLAYER, play.playerID);
		}
	}
	
	if ((jjTriggers[0] && play.xPos >=89*32 && play.xPos < 90*32 && play.yPos >= 57*32 && play.yPos < 62*32+6)
		||
		(play.xPos >=253*32 && play.xPos < 254*32 && play.yPos >= 17*32 && play.yPos < 24*32+6))
			play.xPos = play.xPos + 16;
	}
}

bool onCheat(string &in cheat) { // made for testing but do go off
	if (cheat == "jjcracco") {
			if (!craccoMode) { getFunky(jjLocalPlayers[0]); }
			else { stopFunky(jjLocalPlayers[0]); }
			jjAlert("jjcracco", size: STRING::MEDIUM);
			return true;
	}
	if (cheat == "jjfast") {
		fastFeetMode[0] = 1400; // can only cheat in SP mode anyway
		if (storeMusic == "BEER.it") jjSetModTempo(uint8(defaultModTempo[0] * 1.33));
		else if (storeMusic == "DD-BossGBA.it") jjSetModTempo(uint8(defaultModTempo[1] * 1.33));
		else if (jjMusicFileName != "DD-CastleFast.it") {
			storeMusic = jjMusicFileName;
			jjMusicLoad("DD-CastleFast.it");
		}
		jjAlert("jjfast", size: STRING::MEDIUM);
		return true;
	}
	if (levelOver || inScene || CurrentPopup !is null) return true;
	else return false;
}

bool onDrawScore(jjPLAYER@ player, jjCANVAS@ canvas) {
	if (computerOn) {
		canvas.drawSprite((jjResolutionWidth / 2) - 16, jjResolutionHeight - (jjResolutionHeight / 32), ANIM::CUSTOM[11], 0, (jjGameTicks/5) & 15);
		canvas.drawString(jjResolutionWidth / 2, jjResolutionHeight - (jjResolutionHeight / 32), "x" + player.gems[GEM::PURPLE], STRING::MEDIUM);
	}
	if (fastFeetMode[player.playerID] > 210 || (fastFeetMode[player.playerID] > 0 && jjGameTicks % 2 == 0)) canvas.drawString((jjSubscreenWidth / 2) - (jjGetStringWidth("GOTTA GO FAST 00", STRING::MEDIUM, STRING::BOUNCE) / 2), 50, "GOTTA GO FAST " + uint(fastFeetMode[player.playerID] / 70), STRING::MEDIUM, STRING::BOUNCE, 16);
	if (CurrentPopup !is null) {
		CurrentPopup.Draw(canvas);
		return !CurrentPopup.DrawHUD();
	}
	if (levelOver) {
		drawFade(player, canvas);
		if (rankLine < 6) rankingDisplay(player, canvas);
	}
	if (rankLine >= 6 || inScene) 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 || inScene) return true;
	else return CurrentPopup !is null && !CurrentPopup.DrawHUD();
}

bool onDrawHealth(jjPLAYER@ player, jjCANVAS@ canvas) {
	if (rankLine >= 6 || inScene) return true;
	else return CurrentPopup !is null && !CurrentPopup.DrawHUD();
}

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

bool onDrawPlayerTimer(jjPLAYER@ play, jjCANVAS@ canvas) {
	if (inScene) return true;
	else return CurrentPopup !is null && !CurrentPopup.DrawHUD();
}

//applying bones to layer:
void onDrawLayer1(jjPLAYER@ player, jjCANVAS@ canvas) {
	for (int i = 0; i < 50; i++) canvas.drawSprite(224 * i, 800, ANIM::HELMUT, 0, 2);
}

void onDrawLayer4(jjPLAYER@ player, jjCANVAS@ canvas) {
	for (uint i = 0; i < spiderTileX.length; i++) {
		if (spiderTileX[i] != 0 || spiderTileY[i] != 0) {
            canvas.drawSprite(spiderTileX[i] * 32 + 16, spiderTileY[i] * 32 + 16, ANIM::CUSTOM[31], 1, jjParameterGet(spiderTileX[i], spiderTileY[i], 0, 2) - 1);
		}
	}
	canvas.drawSprite((559 * 32) + 16, (34 * 32) + 22, ANIM::BONUS, 0, (jjGameTicks/8) & 9, 1);
	canvas.drawSprite((559 * 32) + 16, (40 * 32) + 22, ANIM::BONUS, 0, (jjGameTicks/8) & 9, 1);
	jjTEXTAPPEARANCE appearance(STRING::BOUNCE);
	appearance.spacing = 1;
	canvas.drawString((559 * 32) + 3, (34 * 32) + 7, "50", STRING::SMALL, appearance, 6, SPRITE::PALSHIFT, 208);
	canvas.drawString((559 * 32) + 3, (40 * 32) + 7, "30", STRING::SMALL, appearance, 6, SPRITE::PALSHIFT, 208);
	if (!jjTriggers[7]) canvas.drawSprite((688 * 32) - 1, 48 * 32, ANIM::CUSTOM[36], 7, 0, 1, SPRITE::BLEND_NORMAL, saberFade);
}

void onDrawLayer5(jjPLAYER@ player, jjCANVAS@ canvas) { // optimize this later
	canvas.drawSprite((688 * 32) + 16, 44 * 32, ANIM::CUSTOM[34], 0, 0, 64, SPRITE::TINTED, 79);
	canvas.drawSprite((694 * 32) + 16, 40 * 32, ANIM::CUSTOM[34], 0, 0, 64, SPRITE::TINTED, 79);
	canvas.drawSprite((695 * 32) + 16, (40 * 32) + 16, ANIM::CUSTOM[34], 0, 0, 64, SPRITE::TINTED, 79);
	canvas.drawSprite((696 * 32) + 16, 41 * 32, ANIM::CUSTOM[34], 0, 0, 64, SPRITE::TINTED, 79);
	canvas.drawSprite((697 * 32) + 16, 43 * 32, ANIM::CUSTOM[34], 0, 0, 64, SPRITE::TINTED, 79);
	canvas.drawSprite((699 * 32) + 16, 44 * 32, ANIM::CUSTOM[34], 0, 0, 64, SPRITE::TINTED, 79);
	canvas.drawSprite((700 * 32) + 16, 45 * 32, ANIM::CUSTOM[34], 0, 0, 64, SPRITE::TINTED, 79);
	canvas.drawSprite((701 * 32) + 16, 46 * 32, ANIM::CUSTOM[34], 0, 0, 64, SPRITE::TINTED, 79);
	canvas.drawSprite((687 * 32) + 16, 50 * 32, ANIM::CUSTOM[34], 0, 0, 0, SPRITE::TINTED, 79);
	canvas.drawSprite((689 * 32) + 16, 50 * 32, ANIM::CUSTOM[34], 0, 0, 0, SPRITE::TINTED, 79);
	canvas.drawSprite((695 * 32) + 16, 50 * 32, ANIM::CUSTOM[34], 0, 0, 0, SPRITE::TINTED, 79);
	canvas.drawSprite((696 * 32) + 16, 50 * 32, ANIM::CUSTOM[34], 0, 0, 0, SPRITE::TINTED, 79);
	canvas.drawSprite((706 * 32) + 16, 50 * 32, ANIM::CUSTOM[34], 0, 0, 0, SPRITE::TINTED, 79);
	canvas.drawSprite((707 * 32) + 16, 50 * 32, ANIM::CUSTOM[34], 0, 0, 0, SPRITE::TINTED, 79);
}

void onDrawLayer8(jjPLAYER@ play, jjCANVAS@ canvas) {
	canvas.drawSprite(jjSubscreenWidth, int(jjSubscreenHeight * 0.8), ANIM::HELMUT, 0, smallScreen ? 1 : 0);
	if (smallScreen) {
		for (int i = 1; i < 4; i++) 
			canvas.drawSprite(jjSubscreenWidth + (480 * i), int(jjSubscreenHeight * 0.8), ANIM::HELMUT, 0, 1);
	}
	else canvas.drawSprite(jjSubscreenWidth + 1920, int(jjSubscreenHeight * 0.8), ANIM::HELMUT, 0, 0);
}

void deleteUnwantedEvents() {
    int width = jjLayerWidth[4];
    int height = jjLayerHeight[4] - 1;
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            int event = jjEventGet(j, i);
            if (event == 254)
                jjParameterSet(j, i + 1, -12, 32, 0);
        }
    }
}

void LoopObject(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.state = STATE::ROTATE;
		obj.var[0] = jjParameterGet(uint(obj.xOrg) >> 5,uint(obj.yOrg) >> 5, 0,1);
		obj.var[1] = jjParameterGet(uint(obj.xOrg) >> 5,uint(obj.yOrg) >> 5, 1,4);
		obj.doesHurt = 0; //starts at 0, so (LoopsUnmaskedOnLeft != leftSideUnmasked) below will always be true the first time
	} if (obj.state == STATE::DEACTIVATE)
		obj.deactivate();
	else {
		const auto nearestPlayerID = obj.findNearestPlayer(960*960);
		if (nearestPlayerID >= 0) {
			const jjPLAYER@ play = jjPlayers[nearestPlayerID];
			const uint spacing = obj.var[1];
			uint8 leftSideUnmasked;
			if (play.xPos < obj.xOrg - 32)
				leftSideUnmasked = 2;
			else if (play.xPos > obj.xOrg + spacing*32 + 128)
				leftSideUnmasked = 1;
			else if (PlayerXs[play.localPlayerID].CurrentAngle == LoopAngle::Ceiling)
				leftSideUnmasked = play.direction == -1 ? 2 : 1;
			else
				return;
			if (obj.doesHurt != leftSideUnmasked) {
				obj.doesHurt = leftSideUnmasked;
				const uint xTile = uint(obj.xOrg) >> 5, yTile = uint(obj.yOrg) >> 5;
				const bool grassOnRight = obj.var[0] == 1;
				if (leftSideUnmasked == 2) {
					jjTileSet(4, xTile + 0, yTile, 0);
					jjTileSet(4, xTile + 1, yTile, 0);
					jjTileSet(4, xTile + spacing + 2, yTile, grassOnRight ? 323 : 306);
					jjTileSet(4, xTile + spacing + 3, yTile, grassOnRight ? 324 : 307);
					jjTileSet(4, xTile + 0, yTile - 1, 0);
					jjTileSet(4, xTile + spacing + 3, yTile - 1, 297);
					jjEventSet(xTile + 1, yTile, 0);
					jjEventSet(xTile + spacing + 2, yTile, CornerEventID);
				} else {
					jjTileSet(4, xTile + 0, yTile, grassOnRight ? 304 : 325);
					jjTileSet(4, xTile + 1, yTile, grassOnRight ? 305 : 326);
					jjTileSet(4, xTile + spacing + 2, yTile, 0);
					jjTileSet(4, xTile + spacing + 3, yTile, 0);
					jjTileSet(4, xTile + 0, yTile - 1, 294);
					jjTileSet(4, xTile + spacing + 3, yTile - 1, 0);
					jjEventSet(xTile + 1, yTile, CornerEventID);
					jjEventSet(xTile + spacing + 2, yTile, 0);
				}
			}
		}
	}
}

void AddCornerEvents() {
	for (int xTile = jjLayerWidth[4]; --xTile >= 0;)
		for (int yTile = jjLayerHeight[4]; --yTile >= 0;) {
			LoopAngle inputAngle = LoopAngle::Floor;
			bool turnRight = false;
			bool isLoop = false;
			int xOffset = 0, yOffset = 0;
			switch (jjTileGet(4, xTile, yTile)) {
				case 677:
				case 855:
				case 2284:
				case 2352:
					yOffset = -1;
					break;
				case 686:
				case 864:
				case 2293:
				case 2361:
					xOffset = -1;
					turnRight = true;
					inputAngle = LoopAngle::WallOnRight;
					break;
				case 685:
				case 819:
				case 2262:
				case 2362:
					xOffset = 1;
					turnRight = true;
					inputAngle = LoopAngle::WallOnLeft;
					break;
				case 694:
				case 828:
				case 2271:
				case 2371:
					yOffset = 1;
					inputAngle = LoopAngle::Ceiling;
					break;
				case 670:
				case 852:
				case 2280:
				case 2353:
					yOffset = -1;
					turnRight = true;
					break;
				case 681:
				case 863:
				case 2291:
				case 2364:
					xOffset = 1;
					inputAngle = LoopAngle::WallOnLeft;
					break;
				case 682:
				case 838:
				case 2292:
				case 2363:
					xOffset = -1;
					inputAngle = LoopAngle::WallOnRight;
					break;
				case 693:
				case 849:
				case 2303:
				case 2374:
					yOffset = 1;
					turnRight = true;
					inputAngle = LoopAngle::Ceiling;
					break;
				case 956:
					yOffset = 1;
					isLoop = true;
					turnRight = true;
					inputAngle = LoopAngle::Ceiling;
					break;
				case 696:
				case 866:
				case 2369:
					xOffset = -1;
					isLoop = true;
					turnRight = true;
					inputAngle = LoopAngle::WallOnRight;
					break;
				case 714:
				case 874:
				case 2387:
					yOffset = -1;
					isLoop = true;
					break;
				case 713:
				case 873:
				case 2377:
					yOffset = -1;
					isLoop = true;
					turnRight = true;
					break;
				case 691:
				case 861:
				case 2355:
					xOffset = 1;
					isLoop = true;
					inputAngle = LoopAngle::WallOnLeft;
					break;
				case 3600:
					xOffset = 1;
					isLoop = true;
					turnRight = true;
					inputAngle = LoopAngle::WallOnLeft;
					break;
				case 3601:
					yOffset = 1;
					isLoop = true;
					inputAngle = LoopAngle::Ceiling;
					break;
				case 3602:
					yOffset = 1;
					isLoop = true;
					turnRight = true;
					inputAngle = LoopAngle::Ceiling;
					break;
				case 3603:
					xOffset = -1;
					isLoop = true;
					inputAngle = LoopAngle::WallOnRight;
					break;
				default:
					continue;
			}
			if (jjEventGet(xTile + xOffset, yTile + yOffset) == 0) {
				jjEventSet(xTile + xOffset, yTile + yOffset, CornerEventID);
				jjParameterSet(xTile + xOffset, yTile + yOffset, 0,4, (turnRight ? 1 : 0) | (isLoop ? 0 : 2) | (inputAngle << 2));
			}
		}
}

class Ropeway : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		jjPLAYER@ play = jjPlayers[obj.var[0]];
		bool isRealPlayer = obj.var[0] >= 0 && PlayerXs[play.localPlayerID].State == PlayerState::Ropeway;
		if (obj.state == STATE::START) {
			obj.state = STATE::WAIT;
			obj.var[0] = -1; //no player yet
			const uint xTile = uint(obj.xOrg) >> 5, yTile = uint(obj.yOrg) >> 5;
			obj.direction = jjParameterGet(xTile,yTile, 0,1) * 2 - 1;
			obj.special = jjParameterGet(xTile,yTile, 1,1); //rotates
		} else if (obj.state == STATE::DEACTIVATE) {
			reset(obj);
		} else if (obj.state == STATE::ROTATE) {
			obj.special += int(obj.xAcc * 3 * obj.direction);
			const float sin = jjSin(obj.special), cos = jjCos(obj.special);
			jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos - 24, obj.curFrame, obj.special);
			if (isRealPlayer) {
				jjDrawRotatedSprite(play.xPos = (obj.xPos + sin * 30), play.yPos = (obj.yPos + cos * 30 - 24), play.setID, RABBIT::HANGIDLE2, jjGameTicks>>3, obj.special, play.direction = obj.direction,1, SPRITE::PLAYER, obj.var[0]);
				if (play.keyJump) {
					release(obj);
					obj.counter = 200;
				}
				obj.counterEnd = jjSampleLooped(obj.xPos, obj.yPos, SOUND::COMMON_FOEW5, obj.counterEnd, int(abs(cos) * 46) + 15);
			} else
				release(obj);
		} else {
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos - 24, obj.curFrame);
			if (obj.state == STATE::ACTION) {
				if (isRealPlayer) {
					jjDrawSprite(play.xPos = obj.xPos, (play.yPos = obj.yPos) + 6, play.setID, play.keyFire ? RABBIT::HANGINGFIRERIGHT : RABBIT::HANGIDLE2, jjGameTicks>>3, play.direction = obj.direction, SPRITE::PLAYER, obj.var[0]);
					if (play.keyJump && obj.xAcc > 3.f) {
						release(obj);
						play.ySpeed = -5;
						obj.state = STATE::ACTION;
					}
				}
				if (obj.xAcc < 8.f)
					obj.xAcc += 0.1f;
				if (jjGameTicks & 1 == 0) {
					jjPARTICLE@ part = jjAddParticle(PARTICLE::SPARK);
					if (part !is null) {
						part.xPos = obj.xPos - obj.direction*7;
						part.yPos = obj.yPos - 21;
						part.xSpeed = -obj.direction;
						part.ySpeed = 0.5;
					}
				}
				obj.xPos += obj.xAcc * obj.direction;
				obj.yPos += obj.xAcc / 2;
				obj.counterEnd = jjSampleLooped(obj.xPos, obj.yPos, SOUND::COMMON_FOEW5, obj.counterEnd);
				const auto newTileID = jjTileGet(4, uint(obj.xPos)>>5, uint(obj.yPos)>>5);
				if (newTileID == 1500 || newTileID == 1510 || newTileID == 1520) { //bg pillar
					obj.xPos += obj.xAcc * obj.direction * 2;
					obj.yPos += obj.xAcc;
					if (obj.special == 1 && isRealPlayer)
						obj.state = STATE::ROTATE;
					else
						release(obj);
				}
			} else if (obj.state == STATE::DONE) {
				if (++obj.counter > 210) {
					obj.particlePixelExplosion(0);
					reset(obj);
				}
			}
		}
	}
	void reset(jjOBJ@ obj) const {
		obj.state = STATE::START;
		obj.xPos = obj.xOrg;
		obj.yPos = obj.yOrg;
		obj.playerHandling = HANDLING::PICKUP;
		obj.counter = 0;
	}
	void release(jjOBJ@ obj) const {
		if (obj.var[0] >= 0) {
			jjPLAYER@ play = jjPlayers[obj.var[0]];
			PlayerX@ playX = PlayerXs[play.localPlayerID];
			if (playX.State == PlayerState::Ropeway) {
				playX.State = PlayerState::Normal;
				const float xThrust = obj.xAcc * obj.direction * 2;
				play.xAcc = jjCos(obj.special) * xThrust;
				play.ySpeed = -jjSin(obj.special) * xThrust;
				play.direction = obj.direction;
				play.invisibility = false;
			}
		}
		obj.state = STATE::DONE;
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) {
		PlayerX@ playX = PlayerXs[play.localPlayerID];
		if (obj.var[0] == -1 && playX.State == PlayerState::Normal && force != -1) {
			obj.var[0] = play.playerID;
			obj.state = STATE::ACTION;
			obj.playerHandling = HANDLING::PARTICLE; //no further collisions
			obj.xAcc = 0;
			playX.State = PlayerState::Ropeway;
			play.invisibility = true;
		}
		return true;
	}
}

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 bombScenEdit(jjOBJ@ obj) {
	const array<float> crateX = {347*32+16,350*32+16};
	const array<float> crateY = {18*32+16,28*32+16};
	obj.behave(BEHAVIOR::DESTRUCTSCENERY);
	if (obj.isActive) obj.var[5] = jjParameterGet(int(obj.xOrg / 32), int(obj.yOrg / 32), 0, 2);
	if (obj.var[5] != 0) { // block flagged for tracking puzzle progress
		if (obj.isActive && obj.age == 0) {
			jjOBJ@ spawn;
			if (obj.var[6] == 0) {
				@spawn = jjObjects[jjAddObject(OBJECT::EXPLOSION, crateX[obj.var[5] - 1], crateY[obj.var[5] - 1])];
				spawn.determineCurAnim(ANIM::PICKUPS, 86); // twinkle twinkle little star
			} 
			if (obj.var[6] == 40) {
				// Create new crate
				jjOBJ@ crate;
				@crate = jjObjects[jjAddObject(OBJECT::BOMBCRATE, crateX[obj.var[5] - 1], crateY[obj.var[5] - 1])];
				crate.deactivates = false;
				if (obj.var[7] == 1) crate.points = 0; // already got points for it once, now worthless (like generator obj)
				obj.age = crate.objectID; // Store the ID
				obj.counter = 0;
			} else obj.var[6] = obj.var[6] + 1;
		}
		
		// Check if crate still exists by looking up the stored ID
		if (obj.state == STATE::SLEEP && obj.age != 0) {
			jjOBJ@ crate = jjObjects[obj.age];
			if (!crate.isActive) {
				if (obj.counter == 70) {
				// Crate was destroyed - reset so we can create a new one
				obj.var[6] = 0;
				obj.age = 0;
				} else obj.counter++;
			} else if (crate.state == STATE::KILL) obj.var[7] = 1;
		}
	}
	obj.deactivates = false; // only run age == 0 once
	if (obj.state == STATE::KILL) {
		jjOBJ@ crate = jjObjects[obj.age];
		crate.deactivates = true; // no longer need to keep it around
	}
}

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 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 FastFeet : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::PICKUP);
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@, jjPLAYER@ player, int) {
		fastFeetMode[player.playerID] = 1400;
		if (storeMusic == "BEER.it") jjSetModTempo(uint8(defaultModTempo[0] * 1.33));
		else if (storeMusic == "DD-BossGBA.it") jjSetModTempo(uint8(defaultModTempo[1] * 1.33));
			else if (jjMusicFileName != "DD-CastleFast.it" && noFast) {
				storeMusic = jjMusicFileName;
				jjMusicLoad("DD-CastleFast.it");
			}
		obj.behavior = BEHAVIOR::EXPLOSION2;
		obj.scriptedCollisions = false;
		obj.frameID = 0;
		return true;
	}
}

class bombDemon : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
		int nearPlayerID = obj.findNearestPlayer(0x2000);
		bool onPlatform = false;
		float platformOffset = 0.f;
		float py = frame.hotSpotY - frame.coldSpotY;
		
		if (obj.isActive && obj.state != STATE::KILL) {
			for (int i = 1; i < jjObjectCount; i++) {
				jjOBJ@ other = jjObjects[i];
				if ((other.eventID == 231 || other.eventID == 232) && other.yPos > obj.yPos + 50 && other.yPos < obj.yPos + 58 && abs(other.xPos - obj.xPos) <= 30) {
					onPlatform = true;
					platformOffset = abs(other.xPos - obj.xPos);
					obj.ySpeed = 0;
					obj.xPos = obj.xPos + other.xSpeed;
					break;
				}
				if ((other.eventID == 231 || other.eventID == 232) && obj.state == STATE::WALK && abs(other.yPos - obj.yPos) <= 24 && int(other.xPos) == int(obj.xPos + (36 * obj.direction))) {
					obj.counter = 0;
					obj.frameID = 0;
					obj.determineCurAnim(ANIM::TURTLE, 2);
					jjSample(obj.xPos, obj.yPos, SOUND::FROG_FROG5);
					obj.xPos -= 2 * obj.direction;
					obj.state = STATE::TURN;
				}
				if ((other.eventID == 85 || other.eventID == 86 || other.eventID == 87) && obj.doesCollide(other)) {
					obj.counter = 0;
					obj.ySpeed = 81.0 - float(other.eventID);
					obj.determineCurAnim(ANIM::TURTLE, 5);
					obj.state = STATE::FALL;
					other.state = STATE::SPRING;
					jjSample(other.xPos, other.yPos, SOUND::SPRING_SPRING1);
				}
			}
		}

		//jjPrint(""+obj.state + "   " + onPlatform + "   " + platformOffset + "   " + obj.direction);
				
		switch (obj.state) {
			case STATE::START:
				if (obj.creatorType == CREATOR::LEVEL) obj.var[7] = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,1);
				obj.var[5] = 0; // idle counter
				obj.counter = 0;
				obj.counterEnd = 64;
				obj.putOnGround();
				if (jjRandom()%2 == 0) obj.direction = -1;
				else obj.direction = 1;
				obj.xSpeed = obj.direction * 1;
				obj.ySpeed = 0;
				obj.yAcc = 0.1;
				obj.determineCurAnim(ANIM::TURTLE, 7);
				obj.state = STATE::WALK;
				
			case STATE::WALK:
				obj.frameID = ((obj.counter / 5) % jjAnimations[obj.curAnim].frameCount);
				obj.counter++;
				obj.var[5] = obj.var[5] + 1;
				if (obj.counterEnd > 0) obj.counterEnd--;
				obj.determineCurFrame();
				
				if (obj.counter % 5 == 0) {
					if (obj.frameID == 5) jjSample(obj.xPos, obj.yPos, SOUND::TURTLE_SPK1TURT);
					else if (obj.frameID == 10) jjSample(obj.xPos, obj.yPos, SOUND::TURTLE_SPK2TURT);
				}
				if (nearPlayerID > -1 && obj.counter > 20 - (jjDifficulty * 5)) { // attack trigger
					if (((obj.direction < 0 && jjLocalPlayers[nearPlayerID].xPos < obj.xPos) ||
						(obj.direction > 0 && jjLocalPlayers[nearPlayerID].xPos > obj.xPos)) &&
						abs(jjLocalPlayers[nearPlayerID].yPos - obj.yPos) < 64 &&
						obj.counterEnd == 0) {
						obj.determineCurAnim(ANIM::TURTLE, 0);
						obj.counter = 0;
						obj.frameID = 0;
						jjSample(obj.xPos, obj.yPos, SOUND::TURTLE_NECK);
						obj.state = STATE::ATTACK;
					}
				}
				
				if (obj.var[5] > 224 + (jjDifficulty * 16) + (jjRandom() % 32)) { // idle trigger
					obj.counter = 0;
					obj.frameID = 0;
					obj.determineCurAnim(ANIM::TURTLE, 1);
					obj.state = STATE::IDLE;
				}
				
				if (((!jjMaskedPixel(int(obj.xPos + obj.xSpeed), int(obj.yPos + py)) && obj.var[7] == 1) || !jjMaskedPixel(int(obj.xPos - (obj.direction * 2)), int(obj.yPos + py))) && !onPlatform) { // fall or turn trigger
					obj.counter = 0;
					obj.determineCurAnim(ANIM::TURTLE, 5);
					obj.state = STATE::FALL;
				}				
				else if (
						(!onPlatform && (!jjMaskedVLine(int(obj.xPos + obj.xSpeed), int(obj.yPos + py), 2)
						||
						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))
						^^
						(onPlatform && obj.var[7] == 0 && platformOffset >= 27.f) // don't go *too* close to 30.f
					) {	
						obj.counter = 0;
						obj.frameID = 0;
						obj.determineCurAnim(ANIM::TURTLE, 2);
						jjSample(obj.xPos, obj.yPos, SOUND::FROG_FROG5);
						obj.xPos -= 2 * obj.direction;
						obj.state = STATE::TURN;
					}
					else obj.xPos = obj.xPos + obj.xSpeed;
				break;
			
			case STATE::IDLE:
				if (!onPlatform && !jjMaskedVLine(int(obj.xPos), int(obj.yPos + py), 2)) { // GROUND NOT IS THERE (GONE) (STOLEM)
					obj.counter = 0;
					obj.determineCurAnim(ANIM::TURTLE, 5);
					obj.state = STATE::FALL;
				}
				if (nearPlayerID > -1 && obj.counter > 20 - (jjDifficulty * 5)) { // unlike turtle can still bite...
					if (((obj.direction < 0 && jjLocalPlayers[nearPlayerID].xPos < obj.xPos) ||
						(obj.direction > 0 && jjLocalPlayers[nearPlayerID].xPos > obj.xPos)) &&
						abs(jjLocalPlayers[nearPlayerID].yPos - obj.yPos) < 64 &&
						obj.counterEnd == 0) {
						obj.determineCurAnim(ANIM::TURTLE, 0);
						obj.counter = 0;
						obj.frameID = 0;
						obj.state = STATE::ATTACK;
					}
				}
				if (obj.counterEnd > 0) obj.counterEnd--;
				obj.counter++;
				if (obj.counter % 7 == 0) {
					obj.frameID++;
					if (obj.frameID == 6) jjSample(obj.xPos, obj.yPos, SOUND::TURTLE_IDLE1, 63, jjRandom()%2 == 0 ? 48000 : 38097);
					if (uint(obj.frameID) > jjAnimations[obj.curAnim].frameCount) {
					obj.frameID = 0;
					obj.counter = 0;
					obj.var[5] = 0;
					obj.determineCurAnim(ANIM::TURTLE, 7);
					obj.state = STATE::WALK;
					}
				}
				obj.determineCurFrame();
				break;
			
			case STATE::TURN:
				if (!jjMaskedPixel(int(obj.xPos + obj.xSpeed), int(obj.yPos + py)) && obj.var[7] == 1) obj.state == STATE::FALL;
				obj.counter++;
				if (obj.counter % 7 == 0) obj.frameID++;
				if (uint(obj.frameID) == jjAnimations[obj.curAnim].frameCount) {
					obj.frameID = 0;
					obj.counter = 0;
					if (uint(obj.curAnim) == jjAnimSets[ANIM::TURTLE].firstAnim + 2) {
						obj.determineCurAnim(ANIM::TURTLE, 3);
						obj.direction = obj.direction * -1;
						obj.xSpeed = obj.xSpeed * -1;
						jjSample(obj.xPos, obj.yPos, SOUND::FROG_FROG2);
					} else {
						obj.determineCurAnim(ANIM::TURTLE, 7);
						obj.state = STATE::WALK;
					}
				}
				obj.determineCurFrame();
				break;
				
			case STATE::ATTACK:
				obj.counter++;
				if (obj.counter % 4 == 0) {
					obj.frameID++;
					obj.determineCurFrame();
					if (obj.frameID == 3) jjSample(obj.xPos, obj.yPos, SOUND::TURTLE_BITE3);
					if (uint(obj.frameID) > jjAnimations[obj.curAnim].frameCount) {
						obj.determineCurAnim(ANIM::TURTLE, 7);
						obj.counter = 0;
						obj.counterEnd = 96;
						obj.frameID = 0;
						obj.state = STATE::WALK;
					}
				}
				break;
				
			case STATE::FALL:
				obj.frameID = (jjGameTicks/7) & 1;
				obj.determineCurFrame();
				obj.special++; // fall timer
				if (jjMaskedPixel(int(obj.xPos), int(obj.yPos - py)) && obj.ySpeed < 0) {
					obj.ySpeed = 1;
				}
				else if (obj.ySpeed < 5) {
					obj.ySpeed += obj.yAcc;
				}
				obj.xPos += obj.xSpeed;
				obj.yPos += obj.ySpeed;
				if (obj.ySpeed > 0 && (jjMaskedPixel(int(obj.xPos + obj.xSpeed), int(obj.yPos + obj.ySpeed + py)) || onPlatform)) { // landing detection
					if (obj.special > 10) jjSample(obj.xPos, obj.yPos, SOUND::FROG_FROG2); // don't play sound if the drop is tiny
					obj.determineCurAnim(ANIM::TURTLE, 7);
					obj.ySpeed = 0;
					obj.special = 0;
					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:
				if (obj.creatorType == CREATOR::OBJECT) obj.var[8] = 0; // if I'm thinking right this should be exactly one tick
				obj.delete();
				break;
		}
	}
	void onDraw(jjOBJ@ obj) {
		if (obj.var[7] == 1) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::PLAYER, jjPlayers[31].playerID);
		else obj.draw();
//		jjDrawString(obj.xPos, obj.yPos - 64, formatInt(obj.counter) + " " + formatInt(obj.frameID), STRING::MEDIUM);
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
        int lastHitID = player.playerID;
        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.objectID == 0) obj.energy = 0;
            else {
                obj.energy -= bullet.animSpeed;
                obj.justHit = 5;
            }
            if (obj.energy <= 0) {
                if (obj.var[9] == 0) {
                    jjOBJ@ bomb = jjObjects[jjAddObject(OBJECT::TURTLESHELL, obj.xPos, obj.yPos)];
                    obj.var[9] = 1;
                    bomb.ySpeed = 4;
                    bomb.xSpeed = bullet.xSpeed * (0.1 + (jjRandom() % 10 * 0.1));
                    bomb.determineCurAnim(ANIM::TURTLE, 4);
                    bomb.var[6] = lastHitID;
                    bomb.var[7] = obj.var[7];
                    playRandomSwish(obj.xPos, obj.yPos);
                }
                if (obj.creatorType == CREATOR::OBJECT) obj.var[8] = 1;
                obj.state = STATE::KILL;
                obj.grantPickup(jjLocalPlayers[lastHitID], bullet.animSpeed == 0 ? 10 : bullet.animSpeed * 5);
                givePlayerPointsForObject(jjLocalPlayers[lastHitID], obj);
            }
        }
        else if (force == 0) { //collision
                if (obj.state != STATE::FREEZE)
                    player.hurt();
        } else {
            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) { //frozen+running
                player.xAcc = 0;
                player.xSpeed /= -2;
                player.ySpeed = -6;
                player.extendInvincibility(-10);
            }
            if (obj.var[9] == 0) {
                jjOBJ@ bomb = jjObjects[jjAddObject(OBJECT::TURTLESHELL, obj.xPos, obj.yPos)];
                obj.var[9] = 1;
                bomb.ySpeed = 4;
                bomb.xSpeed = (jjRandom() % 4 - 2) * 0.1;
                bomb.determineCurAnim(ANIM::TURTLE, 4);
                bomb.var[6] = lastHitID;
                bomb.var[7] = obj.var[7];
                playRandomSwish(obj.xPos, obj.yPos);
            }
            obj.state = STATE::KILL;
            givePlayerPointsForObject(jjLocalPlayers[lastHitID], obj);
        }
        return true;
    }
}

class demonBomb : jjBEHAVIORINTERFACE {
    demonBomb(jjOBJ@ objectPreset) {
        objectPreset.behavior = this;
        objectPreset.playerHandling = HANDLING::SPECIAL;
        objectPreset.bulletHandling = HANDLING::DETECTBULLET;
        objectPreset.scriptedCollisions = true;
        objectPreset.isTarget = false;
        objectPreset.isFreezable = false;
        objectPreset.triggersTNT = false;
        objectPreset.deactivates = true;
    }
    
    void onBehave(jjOBJ@ obj) {
        float dx, dy, distance;
        if (obj.state == STATE::START) {
            obj.counterEnd = 250;
            obj.state = STATE::FLY;
        }
        
        if (obj.state == STATE::FLY) {
            int calc = 0;
            int calc2 = 0;
            obj.counterEnd--;
            if (obj.counterEnd < 125 && jjGameTicks % 10 == 0) {
                obj.justHit = 5;
            }
            
            obj.xSpeed *= 0.984375;
            obj.xPos += obj.xSpeed;
            obj.yPos += obj.ySpeed;
            
            bool noUp = jjMaskedPixel(int(obj.xPos), int(obj.yPos) - 4);
            bool noDown = jjMaskedPixel(int(obj.xPos), int(obj.yPos) + 16);
            bool noLeft = jjMaskedPixel(int(obj.xPos) - 8, int(obj.yPos));
            bool noRight = jjMaskedPixel(int(obj.xPos) + 8, int(obj.yPos));
            
            if (abs(obj.xSpeed) > 0.015625) {
                for (int i = 1; i < jjObjectCount; i++) {
                    jjOBJ@ other = jjObjects[i];
                    if (other.isActive && (other.playerHandling == HANDLING::ENEMY || other.playerHandling == HANDLING::SPECIAL) && obj.doesCollide(other) && other.eventID != 237) {
                        if (jjPlayers[obj.var[6]].objectHit(other, -1, other.playerHandling)) {
                            if (obj.xSpeed > 0) noRight = true;
                            else if (obj.xSpeed < 0) noLeft = true;
                        }
                    }
                }
            }
            
			for (int i = 1; i < jjObjectCount; i++) {
                    jjOBJ@ other = jjObjects[i];
					if (other.eventID == OBJECT::DESTRUCTSCENERY && other.state == STATE::SLEEP && other.var[6] == 0 && obj.doesCollide(other)) {
						other.state = STATE::KILL;
						givePlayerPointsForObject(jjLocalPlayers[obj.var[6]], other);
					}
                }
			
            if (obj.xSpeed > 0 && noRight) {
                calc = 1 + int(obj.xSpeed);
                calc2 = jjMaskedTopVLine(int(obj.xPos + 8), int(obj.yPos) + 16 - calc, calc);
                if (calc2 <= 1 || calc2 >= calc) {
                    obj.xPos -= obj.xSpeed;
                    obj.xSpeed *= -1;
                } else {
                    obj.yPos -= calc;
                    obj.xSpeed *= 0.9375;
                }
            } else if (obj.xSpeed < 0 && noLeft) {
                calc = 1 - int(obj.xSpeed);
                calc2 = jjMaskedTopVLine(int(obj.xPos - 8), int(obj.yPos) + 16 - calc, calc);
                if (calc2 <= 1 || calc2 >= calc) {
                    obj.xPos -= obj.xSpeed;
                    obj.xSpeed *= -1;
                } else {
                    obj.yPos -= calc;
                    obj.xSpeed *= 0.9375;
                }
            }
            
            if (obj.ySpeed < 0 && noUp) {
                obj.yPos -= obj.ySpeed;
                obj.ySpeed = -obj.ySpeed * 0.5;
            } else if (noDown) {
                calc = jjMaskedTopVLine(int(obj.xPos), int(obj.yPos), 16);
                if (calc != 0) {
                    obj.yPos -= 16 - calc;
                } else {
                    obj.yPos -= obj.ySpeed;
                }
                
                if (obj.ySpeed > 1) {
                    playRandomImpact(obj.xPos, obj.yPos);
                }
                
                obj.ySpeed = -obj.ySpeed * 0.5;
            } else {
                if (obj.yPos > jjWaterTarget) {
                    obj.ySpeed += 0.03125;
                    if (obj.ySpeed > 4) obj.ySpeed = 4;
                    else if (obj.ySpeed < -4) obj.ySpeed -4;
                } else {
                    obj.ySpeed += 0.125;
                    if (obj.ySpeed > 8) obj.ySpeed = 8;
                }
            }
        }
        
        if (obj.counterEnd <= 0 && obj.isActive) {
            jjOBJ@ boom = jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos)];
                boom.determineCurAnim(ANIM::AMMO, 81);
                // boom.playerHandling = HANDLING::ENEMYBULLET;
            jjSample(obj.xPos, obj.yPos, SOUND::COMMON_EXPL_TNT);
            for (int i = 1; i < jjObjectCount; i++) {
                jjOBJ@ other = jjObjects[i];
                dx = abs(obj.xPos - other.xPos);
                dy = abs(obj.yPos - other.yPos);
                distance = sqrt(pow(dx, 2) + pow(dy, 2)); // A Perfect Circle...
                if (other.eventID == OBJECT::DESTRUCTSCENERYBOMB && other.state == STATE::SLEEP && distance <= 64) {
                    other.state = STATE::KILL;
                    givePlayerPointsForObject(jjLocalPlayers[obj.var[6]], other);
                } else if (other.bulletHandling == HANDLING::HURTBYBULLET && distance <= 64) {
                    other.energy -= 1;
                    if (other.energy <= 0) givePlayerPointsForObject(jjLocalPlayers[obj.var[6]], other);
                }
                if (obj.findNearestPlayer(64 * 64) > -1) jjLocalPlayers[obj.findNearestPlayer(64 * 64)].hurt(1);
            }
            obj.delete();
        }
		if (obj.state == STATE::DEACTIVATE) obj.delete();
    }
    void onDraw(jjOBJ@ obj) {
        obj.lightType = LIGHT::POINT2;
        obj.light = 100;
        obj.frameID = ((jjGameTicks/5) % jjAnimations[obj.curAnim].frameCount);
        obj.determineCurFrame();
        if (obj.justHit > 0) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 1, SPRITE::SINGLECOLOR, 15, 1);
        else if (obj.var[7] == 1) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::PLAYER, jjPlayers[31].playerID);
        else obj.draw();
    }
    bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
        if (bullet !is null) {
            obj.xSpeed = obj.xSpeed + bullet.xSpeed;
            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;
            jjSample(obj.xPos, obj.yPos, SOUND::TURTLE_HITSHELL);
        }
        obj.var[6] = player.playerID;
        return true;
    }
}

void playRandomImpact(float xPos, float yPos) {
    switch(jjRandom()%8) {
        case 0: jjSample(xPos, yPos, SOUND::COMMON_IMPACT1); break;
        case 1: jjSample(xPos, yPos, SOUND::COMMON_IMPACT2); break;
        case 2: jjSample(xPos, yPos, SOUND::COMMON_IMPACT3); break;
        case 3: jjSample(xPos, yPos, SOUND::COMMON_IMPACT4); break;
        case 4: jjSample(xPos, yPos, SOUND::COMMON_IMPACT5); break;
        case 5: jjSample(xPos, yPos, SOUND::COMMON_IMPACT6); break;
        case 6: jjSample(xPos, yPos, SOUND::COMMON_IMPACT7); break;
        case 7: jjSample(xPos, yPos, SOUND::COMMON_IMPACT8); break;
    }
}

void playRandomSwish(float xPos, float yPos) {
	switch(jjRandom()%8) {
		case 0: jjSample(xPos, yPos, SOUND::COMMON_SWISH1); break;
		case 1: jjSample(xPos, yPos, SOUND::COMMON_SWISH2); break;
		case 2: jjSample(xPos, yPos, SOUND::COMMON_SWISH3); break;
		case 3: jjSample(xPos, yPos, SOUND::COMMON_SWISH4); break;
		case 4: jjSample(xPos, yPos, SOUND::COMMON_SWISH5); break;
		case 5: jjSample(xPos, yPos, SOUND::COMMON_SWISH6); break;
		case 6: jjSample(xPos, yPos, SOUND::COMMON_SWISH7); break;
		case 7: jjSample(xPos, yPos, SOUND::COMMON_SWISH8); break;
	}
}

class bumpFix : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::BUMP);
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (force == -1 && obj.findNearestPlayer(64 * 64) < 0) return true;
		else {
			obj.scriptedCollisions = false;
			if (bullet is null)
				player.objectHit(obj, force, obj.playerHandling);
			else
				bullet.objectHit(obj, obj.playerHandling);
			obj.scriptedCollisions = true;
		return true;
		}
	}
}

class lavaBlock : jjBEHAVIORINTERFACE {
    void onBehave(jjOBJ@ obj) {
        if (obj.var[0] > 0) obj.var[0] = obj.var[0] - 1;
        switch (obj.state) {
            case STATE::START:
                obj.yPos = obj.yOrg + jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,5);
				obj.var[1] = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 5,1);
                obj.state = STATE::IDLE;
            case STATE::IDLE:
                obj.frameID = 0;
                obj.determineCurFrame();
                break;
            case STATE::DEACTIVATE:
                obj.deactivate();
                break;
        }
    }
    void onDraw(jjOBJ@ obj) {
        // don't draw
    }
    
    bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
        if (bullet is null) {
			if (obj.findNearestPlayer(64 * 64) >= 0) {
				if (force <= 0) {
					player.hurt(2);
				}
				if (player.xSpeed * player.xSpeed > 0.2) {
					player.xSpeed = player.xSpeed * 0.1;
				}
				if (player.ySpeed > 0 && obj.var[0] == 0 && obj.var[1] == 0) {
					obj.var[0] = 25;
					playRandomSquish(player.xPos, obj.yPos - 16);
					jjOBJ@ splash = jjObjects[jjAddObject(OBJECT::EXPLOSION, player.xPos, obj.yPos - 16, obj.objectID, CREATOR::OBJECT)];
					splash.determineCurAnim(ANIM::CUSTOM[9], 1);
				}
			}
            return true;
        }
        return false;
    }
}

void playRandomSquish(float xPos, float yPos, int volume = 63) {
	switch(jjRandom()%5) {
	  	case 0: jjSample(xPos, yPos, SOUND::FROG_FROG1, volume); break;
	  	case 1: jjSample(xPos, yPos, SOUND::FROG_FROG2, volume); break;
	  	case 2: jjSample(xPos, yPos, SOUND::FROG_FROG3, volume); break;
	  	case 3: jjSample(xPos, yPos, SOUND::FROG_FROG4, volume); break;
	  	case 4: jjSample(xPos, yPos, SOUND::FROG_FROG5, volume); break;
	}
}

void DecorativeVine(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		const uint eventX = uint(obj.xPos) >> 5, eventY = uint(obj.yPos) >> 5;
		const int length = jjParameterGet(eventX,eventY, 0, 3);
		obj.age = jjRandom();
		obj.curFrame += length;
		obj.var[0] = 50 + length*16;
		obj.var[1] = 4 - jjParameterGet(eventX,eventY, 3, 1);
		obj.xPos += jjParameterGet(eventX,eventY, 4, 3) << 2;
		obj.yPos += jjParameterGet(eventX,eventY, 8, 3) << 2;
		obj.state = STATE::IDLE;
	} else if (obj.state == STATE::DEACTIVATE) {
		obj.deactivate();
	} else {
		if (!jjLowDetail)
			jjDrawSwingingVineSpriteFromCurFrame(int(obj.xPos), int(obj.yPos), obj.curFrame, obj.var[0], int(jjSin(obj.age += 6) * 0x4000), SPRITE::NORMAL,0, obj.var[1]);
			if ((jjSin(obj.age) * 0x4000) % 12000 == 0) playRandomRattle(obj.xPos, obj.yPos);
	}
}

void playRandomRattle(float xPos, float yPos) {
	switch(jjRandom()%2) {
	  	case 0: jjSample(xPos, yPos, SOUND::INTRO_UP1, 63, 42000 + jjRandom()%4000); break;
	  	case 1: jjSample(xPos, yPos, SOUND::INTRO_UP2, 63, 42000 + jjRandom()%4000); break;
	}
}

class pentagram : jjBEHAVIORINTERFACE {
int lastHitID, spawnID;
    pentagram(jjOBJ@ objectPreset) {
        objectPreset.behavior = this;
        objectPreset.playerHandling = HANDLING::SPECIAL;
        objectPreset.scriptedCollisions = true;
        objectPreset.bulletHandling = HANDLING::IGNOREBULLET;
        objectPreset.isTarget = false;
        objectPreset.isFreezable = false;
        objectPreset.triggersTNT = false;
        objectPreset.deactivates = false;
        objectPreset.points = 300;
    }
    void onBehave(jjOBJ@ obj) {
        switch (obj.state) {
            case STATE::START:
                obj.putOnGround();
                obj.counterEnd = 0;
                obj.counter = 245;
                obj.var[1] = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,2);
				obj.var[2] = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 2,1);
                switch (obj.var[1]) {
                    case 0:
                    case 1: spawnID = 117; break;
                    case 2: spawnID = 125; break;
                    case 3:
                    case 4: spawnID = 120; break;
                }
                obj.state = STATE::IDLE;
            case STATE::IDLE:
                if (jjGameTicks % 6 == 0) {
                    jjPARTICLE@ particle = jjAddParticle(PARTICLE::SMOKE);
                    particle.xPos = obj.xPos - 28 + jjRandom() % 56;
                    particle.yPos = obj.yPos - 5 + jjRandom() % 10;
                }
                if (obj.counterEnd < 3 && obj.counter == 40) {
                    jjOBJ@ sparkl = jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos)];
                    sparkl.determineCurAnim(ANIM::PICKUPS, 86);
                }
                if (obj.counterEnd < 3 && obj.counter <= 0) {
                    
                    jjOBJ@ spawn = jjObjects[jjAddObject(spawnID, obj.xOrg - 28 + jjRandom() % 56, obj.yOrg)];
                    spawn.creatorID = uint(obj.objectID);
                    spawn.points = 0; // prevent farming
                    spawn.deactivates = false; // keep detecting deaths
					if (obj.var[2] == 1) spawn.direction = jjRandom() % 2 - 1;
                    if (obj.var[1] == 1 || obj.var[1] == 4) spawn.var[7] = 1;
                    obj.counter = 490;
                    obj.creatorType = CREATOR::OBJECT;
                } else if (obj.counter > 0) obj.counter--;
                
                obj.counterEnd = 0;
                for (int i = 1; i < jjObjectCount; i++) {
                    jjOBJ@ other = jjObjects[i];
                    if (other.creatorID == uint(obj.objectID) && other.isActive) obj.counterEnd += 1;
                }
 
                break;
            case STATE::DEACTIVATE:
                obj.deactivate();
                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::IDLE) {
            obj.determineCurAnim(ANIM::CUSTOM[30], 0);
            obj.frameID = ((jjGameTicks/12) % jjAnimations[obj.curAnim].frameCount);
            obj.determineCurFrame();
            obj.draw();
            obj.lightType = LIGHT::FLICKER;
            obj.light = 100;
        }
        if (obj.state == STATE::KILL) {
            obj.determineCurAnim(ANIM::CUSTOM[30], 1);
            obj.frameID = 0;
            obj.determineCurFrame();
        }
    }
    bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
        if (player !is null && force != 0) {
                lastHitID = player.playerID;
                playRandomSnuff(obj.xPos, obj.yPos);
                obj.state = STATE::KILL;
                givePlayerPointsForObject(jjLocalPlayers[lastHitID], obj);
                obj.particlePixelExplosion(2);
        }
        return true;
    }
    void playRandomSnuff(float xPos, float yPos) {
        switch(jjRandom()%2) {
            case 0: jjSample(xPos, yPos, SOUND::INTRO_HITSPAZ); break;
            case 1: jjSample(xPos, yPos, SOUND::INTRO_HITTURT); break;
        }
    }
}

class littleBat : jjBEHAVIORINTERFACE {
	float offset_x,offset_y,ceiling_y;
	int distance_counter,rolled_angle;
	
	void onBehave(jjOBJ@ obj) {
	//	jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
	//jjDebug("obj.xPos = " + formatFloat(obj.xPos, '', 8, 2));
	//jjDebug("obj.yPos = " + formatFloat(obj.yPos, '', 8, 2));
		switch (obj.state) {
		case STATE::START:
			while (!jjMaskedPixel(int(obj.xPos), int(obj.yPos) - 9)) obj.yPos = obj.yPos - 1; // fall up
			ceiling_y = obj.yPos;
			obj.counter = jjRandom() % 1023;
			//rolled_angle = jjRandom() % 512 - 256;
			//if (rolled_angle < 0) rolled_angle = 1023 - rolled_angle;
			//obj.counter = rolled_angle;
			obj.state = STATE::IDLE;
			break;

		case STATE::IDLE:
			obj.counter++;
			obj.determineCurAnim(ANIM::CUSTOM[12], 2);
			obj.frameID = 0;

			if (obj.findNearestPlayer(128 * 128) >= 0)
			{	
				jjSample(obj.xPos, obj.yPos, SOUND::P2_THROW, 23);
				obj.state = STATE::FLY;
				distance_counter = 64;
				offset_x = (obj.xOrg + jjSin((obj.counter + distance_counter)*4)*32 - obj.xPos)/64;
				//offset_y = (obj.yOrg + jjCos((obj.counter + distance_counter)*2)*24 - obj.yPos)/64;
				offset_y = (ceiling_y + jjCos((obj.counter + distance_counter)*2)*24 - obj.yPos)/64;

			}		
			break;

		case STATE::FLY:
			if (offset_x < 0)
				obj.direction = -1;
			else
				obj.direction = 1;

			obj.xPos += offset_x;
			obj.yPos += offset_y;
			distance_counter -= 1;
			if (distance_counter <= 0) obj.state = STATE::FLOAT;
			obj.counter++;
			obj.determineCurAnim(ANIM::CUSTOM[12], 1);
			obj.frameID = obj.counter / 6 % jjAnimations[obj.curAnim].frameCount;	
			break;

		case STATE::FLOAT:
			obj.counter += 1 + (jjRandom() % 3);
			obj.xPos = obj.xOrg + jjSin(obj.counter * 4) * 32 + (int(jjRandom() % 7) - 3);
			batChannel = jjSampleLooped(obj.xPos, obj.yPos, SOUND::P2_FART, batChannel, 23);
			
			if ((obj.counter % 255) < 128) {
				if (obj.direction > 0)
					playRandomBat(obj.xPos, obj.yPos, 42000 + jjRandom()%4000);
				obj.direction = -1;
			} else {
				if (obj.direction < 0)
					playRandomBat(obj.xPos, obj.yPos, 42000 + jjRandom()%4000);
				obj.direction = +1;
			}
			obj.yPos = obj.yOrg + jjCos(obj.counter * 2) * 24 + (int(jjRandom() % 7) - 3);

			obj.determineCurAnim(ANIM::CUSTOM[12], 1);
			obj.frameID = obj.counter / 6 % jjAnimations[obj.curAnim].frameCount;

			if (obj.findNearestPlayer(256 * 256) == -1)
			{	
				obj.state = STATE::STOP;
				obj.counter = 0;

				distance_counter = 64;
				offset_x = (obj.xOrg - obj.xPos) / 64;
				offset_y = (ceiling_y - obj.yPos) / 64;
			}
			break;

		case STATE::STOP:
			if (offset_x < 0)
				obj.direction = -1;
			else
				obj.direction = 1;

			obj.xPos += offset_x;
			obj.yPos += offset_y;
			distance_counter -= 1;
			if (distance_counter <= 0) {
				obj.state = STATE::IDLE;
				}
			obj.counter++;
			obj.determineCurAnim(ANIM::CUSTOM[12], 1);
			obj.frameID = obj.counter / 6 % jjAnimations[obj.curAnim].frameCount;		
			break;

		case STATE::FREEZE:
			if (obj.freeze > 0) obj.freeze--;
			if (obj.freeze == 0) {
				obj.state = STATE::STOP;
				obj.counter = 0;

				distance_counter = 64;
				offset_x = (obj.xOrg - obj.xPos) / 64;
				offset_y = (ceiling_y - obj.yPos) / 64;
			}

			if (obj.freeze < 30) {
				obj.yPos += 4 * (jjRandom() % 16383);
				obj.state = obj.oldState;
			}

			break;

		case STATE::KILL:	
			obj.delete();
			break;

		case STATE::DEACTIVATE:		
			//jjDebug("obj.xPos = " + formatFloat(obj.xPos, '', 8, 2));
			//jjDebug("obj.yPos = " + formatFloat(obj.yPos, '', 8, 2));
			obj.deactivate();
			break;
		}
	}
	void onDraw(jjOBJ@ obj) {
		if (obj.isActive && obj.state != STATE::KILL) {
			obj.determineCurFrame();
			obj.draw();
		}
	}
	void playRandomBat(float xPos, float yPos, int freq) {
		switch(jjRandom()%4) {
			case 0: jjSample(xPos, yPos, SOUND::P2_FOEW1, 47, freq); break;
			case 1: jjSample(xPos, yPos, SOUND::P2_FOEW4, 47, freq); break;
			case 2: jjSample(xPos, yPos, SOUND::P2_FOEW5, 47, freq); break;
			case 3: jjSample(xPos, yPos, SOUND::P2_POEP, 47, freq); break;
		}
	}
}

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) == 4) obj.determineCurAnim(ANIM::PICKUPS, 92);
			if (jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,3) == 5) obj.determineCurAnim(ANIM::CUSTOM[3], 43);
			if (jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,3) == 6) obj.determineCurAnim(ANIM::PICKUPS, 15);
			}
		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[3], 6);
		if (obj.age == 2) obj.determineCurAnim(ANIM::CUSTOM[3], 42);
		if (obj.age == 3) obj.determineCurAnim(ANIM::CUSTOM[3], 73);
	}
	if (obj.state == STATE::ACTION) {
		if (jjParameterGet(uint(obj.xPos) >> 5, uint(obj.yPos) >> 5, 0,3) < 4) jjSample(obj.xOrg, obj.yOrg, SOUND::INTRO_GRAB);
		else jjSample(obj.xOrg, obj.yOrg, SOUND::MONKEY_SPLUT);
	}
}

class tuffDemon : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::FATCHICK, false);
		if (obj.state == STATE::WALK) {
			obj.determineCurAnim(ANIM::CUSTOM[27], 0);
			obj.frameID = ((obj.counter / 6) % jjAnimations[obj.curAnim].frameCount);
			obj.determineCurFrame();
		}
		if (obj.state == STATE::ACTION) obj.determineCurAnim(ANIM::CUSTOM[27], 1);
	}
	void onDraw(jjOBJ@ obj) {
		obj.draw();
	}
}

class spider : jjBEHAVIORINTERFACE {
int webLength, storeAngle;
jjOBJ@ web;
	void onBehave(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::START:
				webLength = 1;
				@web = jjObjects[jjAddObject(OBJECT::SWINGINGVINE, obj.xPos, obj.yPos)];
				obj.state = STATE::IDLE;
			case STATE::IDLE:
				obj.determineCurAnim(ANIM::CUSTOM[31], 0);
				obj.animSpeed = 12;
				if (obj.findNearestPlayer(0x4000) > -1 && jjLocalPlayers[obj.findNearestPlayer(0x4000)].yPos >= obj.yOrg) obj.state = STATE::ATTACK;
				obj.xPos = obj.xOrg;
				obj.yPos = obj.yOrg;
				break;
			case STATE::ATTACK:
				
				if (webLength < 128) {
					webLength += 4;
					obj.determineCurAnim(ANIM::CUSTOM[31], 2);
					obj.animSpeed = 9;	
				} else {
					obj.determineCurAnim(ANIM::CUSTOM[31], 0);
					obj.animSpeed = 12;
				}
				if (obj.findNearestPlayer(0x10000) == -1 || jjLocalPlayers[obj.findNearestPlayer(0x10000)].yPos < obj.yOrg) obj.state = STATE::HIDE;
				obj.xPos = web.xPos;
				obj.yPos = web.yPos - 8;
				break;
			case STATE::HIDE:
				obj.determineCurAnim(ANIM::CUSTOM[31], 3);
				obj.animSpeed = 9;
				if (webLength == 1) obj.state = STATE::IDLE;
				else webLength -= 2;
				obj.xPos = web.xPos;
				obj.yPos = web.yPos - 16;
				break;
			case STATE::FREEZE:
				if (obj.freeze-- <= 1) {
				obj.freeze = 0;
				obj.state = obj.oldState;
				}
				web.var[3] = storeAngle;
				break;
			case STATE::KILL:
				web.delete();
				obj.delete();
				break;
			case STATE::DEACTIVATE:
				web.delete(); // will be created again when state is start so chill
				obj.deactivate();
				break;
		}
		web.var[1] = webLength;
	}
	void onDraw(jjOBJ@ obj) {
		if (obj.isActive && obj.state != STATE::KILL) {
			obj.frameID = ((jjGameTicks / obj.animSpeed) % jjAnimations[obj.curAnim].frameCount);
			obj.determineCurFrame();
			if (obj.state == STATE::ATTACK || obj.state == STATE::HIDE) jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, int(web.var[2] / 256));
			else obj.draw();
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (bullet !is null && bullet.var[3] == 3) storeAngle = web.var[3];
		return true;
	}
}

class swingingWeb : jjBEHAVIORINTERFACE { // had to add this to obscure a black line leading the vine; unfortunately this draws the web in front of the spider but there doesn't seem to be a better solution
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::SWINGINGVINE, false);
	}
	void onDraw(jjOBJ@ obj) {
		if (obj.var[1] > 1 && obj.var[4] == 0) {
			jjDrawSwingingVineSpriteFromCurFrame(
					obj.xOrg,
					obj.yOrg,
					jjAnimations[jjAnimSets[ANIM::VINE].firstAnim + 1].firstFrame,
					obj.var[1],
					obj.var[2],
					SPRITE::PLAYER,
					jjPlayers[30].playerID
				);
		}
	}
}

class demonEdit : jjBEHAVIORINTERFACE {
    void onBehave(jjOBJ@ obj) {
        switch(obj.state) {
            case STATE::START:
                obj.determineCurAnim(ANIM::DEMON, 0);
                obj.putOnGround();
                obj.state = STATE::IDLE;
                obj.var[7] = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,1);
                obj.xSpeed = obj.ySpeed = obj.xAcc = obj.yAcc = 0; // all initially 0
                break;
            case STATE::IDLE:
                if (obj.ySpeed != 0) {
					obj.determineCurAnim(ANIM::DEMON, 3);
					obj.frameID = 2;
					obj.determineCurFrame();
				} else {
					obj.determineCurAnim(ANIM::DEMON, 0); // additional CPU load but unfortunately necessary
					obj.frameID = (jjGameTicks / 4) % jjAnimations[obj.curAnim].frameCount;
					obj.determineCurFrame();
					if (jjGameTicks % 4 == 0 && obj.frameID == 5) {
						jjSample(obj.xPos, obj.yPos, SOUND::COMMON_HIBELL);
					}
				}
                obj.counter++;
                if (obj.counter > 140) {
                    int playerID = obj.findNearestPlayer(256 * 256);
                    if (playerID >= 0) {
                        jjPLAYER@ victim = jjPlayers[playerID];
                        if (victim.xPos < obj.xPos) {
                            obj.direction = -1;
                        } else if (victim.xPos > obj.xPos) {
                            obj.direction = 1;
                        }
                        
                        if (abs(victim.yPos - obj.yPos) < 64) {
                            obj.frameID = 0;
                            obj.counter = 0;
                            obj.state = STATE::ATTACK;
                            obj.determineCurAnim(ANIM::DEMON, 1);
                            jjSample(obj.xPos, obj.yPos, SOUND::COMMON_FOEW2);
                        }
                    }
                }
                break;
            case STATE::ATTACK:
                obj.counter++;
                if (obj.counter > 4) {
                    obj.frameID++;
                    obj.counter = 0;
                    if (uint16(obj.frameID) >= jjAnimations[obj.curAnim].frameCount) {
                        obj.state = STATE::WALK;
                        obj.determineCurAnim(ANIM::DEMON, 2);
                        obj.counter = 0;
                        obj.frameID = 0;
                        jjSample(obj.xPos, obj.yPos, SOUND::DEMON_RUN);
                    }
                }
                obj.determineCurFrame();
                break;
            case STATE::WALK: // STATE::FALL also merged into this. Inconvenient, I know.
                obj.xSpeed = (2 + jjSin(int(abs(obj.frameID-11))*32)) / 2.0;
                obj.xSpeed *= obj.direction;
                obj.age = 1;
 
                if (jjGameTicks % 8 == 0) {
                    obj.frameID++;
                }
                
                obj.determineCurFrame();
 
                obj.counter++;
                if (obj.counter > 140) {
                    int playerID = obj.findNearestPlayer(96 * 96);
                    if (playerID < 0) {
                        // The original code makes an access to a nonexistent player, which is questionable desgin...
                        // I think it makes more sense to only check playerID < 0 then take action accordingly. If
                        // this doesn't give you desired behavior then remove the uncommented code and uncomment
                        // the below code block, but I'm not responsible for any weird bugs here.
                        /* 
                        float dx = jjPlayers[playerID].xPos - obj.xPos;
                        if ((obj.direction < 0 && dx < 0) || (obj.direction > 0 && dx > 0)) {
                            obj.state = STATE::STOP;
                            obj.counter = 0;
                            obj.frameID = 0;
                            obj.determineCurAnim(ANIM::DEMON, 3);
                        }
                        */
                        obj.state = STATE::STOP;
                        obj.counter = 0;
                        obj.frameID = 0;
                        obj.determineCurAnim(ANIM::DEMON, 3);
                    }
                }
                break;
            case STATE::WAIT:
                obj.frameID = (jjGameTicks / 8) % jjAnimations[obj.curAnim].frameCount;
                obj.determineCurFrame();
                break;
            case STATE::STOP:
                obj.xSpeed = (obj.xSpeed * 7) / 8;
                obj.counter++;
                if (obj.counter > 4) {
                    obj.frameID++;
                    obj.counter = 0;
                    if (uint16(obj.frameID) >= jjAnimations[obj.curAnim].frameCount) {
                        obj.frameID = 0;
                        obj.determineCurAnim(ANIM::DEMON, 0);
                        obj.state = STATE::IDLE;
                        obj.xSpeed = 0;
                        obj.counter = 0;
                    }
                }
                obj.determineCurFrame();
                break;
            // The below states have the least likely side effects with 
            // custom behavior that it doesn't seem worth rewriting them.
            case STATE::FREEZE:
            case STATE::KILL:
            case STATE::DEACTIVATE:
                obj.behave(BEHAVIOR::DEMON);
                break;
        }
        
        // The demon will have "physics" forever from now on when they lock on a target for the first time.
        // This applies to all states! That means a STATE::IDLE demon can also fall (if they were mid-air during STATE::WALK):
        if (obj.age > 0 && obj.state != STATE::FREEZE) { // ok that's one state we actually don't want physics in
            handleMotion(obj, false, false);
            handleFall(obj);
        }
    }
    
    // The original demon behavior caps xSpeed and ySpeed, but if you don't want that then pass false to any of them.
    void handleMotion(jjOBJ@ obj, bool capSpeedX, bool capSpeedY) {
        jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
        float px = 0, py = 0;
        py = frame.hotSpotY - frame.coldSpotY;
        
        if (obj.xSpeed < 0) {
            px = -frame.hotSpotX - frame.width;
        } else {
            px = frame.hotSpotX + frame.width;
        }
        
		// Check for edges if not Pinkie
		if (obj.var[7] == 0 && !jjMaskedPixel(int(obj.xPos + (px * obj.direction) + obj.xSpeed), int(obj.yPos + py))) obj.direction = obj.xSpeed < 0 ? -1 : 1;
		
        // Check for walls
        if (jjMaskedPixel(int(obj.xPos + px), int(obj.yPos))) {
            obj.xSpeed *= -1;
            obj.direction = obj.xSpeed < 0 ? -1 : 1; //  Maybe this needs to be outside?
            obj.xPos += obj.xSpeed;
        }
        
        // and ceilings
        if (jjMaskedPixel(int(obj.xPos), int(obj.yPos - py)) && obj.ySpeed < 0) {
            obj.ySpeed = 0;
        }
        
        obj.xPos += obj.xSpeed;
        obj.yPos += obj.ySpeed;
        if (obj.yPos > jjWaterLevel) {
            obj.xSpeed += obj.xAcc / 4;
            obj.ySpeed += obj.yAcc / 4;
            if (capSpeedX) {
                if (obj.xSpeed > 3) {
                    obj.xSpeed = 3;
                } else if (obj.xSpeed < -3) {
                    obj.xSpeed = -3;
                }
            }
            
            if (capSpeedY) {
                if (obj.ySpeed > 2) {
                    obj.ySpeed = 2;
                } else if (obj.ySpeed < -2) {
                    obj.ySpeed = -2;
                }
            }
        } else {
            obj.xSpeed += obj.xAcc;
            obj.ySpeed += obj.yAcc;
            if (capSpeedX) {
                if (obj.xSpeed > 6) {
                    obj.xSpeed = 6;
                } else if (obj.xSpeed < -6) {
                    obj.xSpeed = -6;
                }
            }
            
            if (capSpeedY) {
                if (obj.ySpeed > 4) {
                    obj.ySpeed = 4;
                } else if (obj.ySpeed < -4) {
                    obj.ySpeed = -4;
                }
            }
        }
    }
    
    void handleFall(jjOBJ@ obj) {
        jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
        float py = frame.hotSpotY - frame.coldSpotY;
        float px = 0;
        
        if (obj.xSpeed < 0) {
            px = -frame.hotSpotX - (frame.width / 2);
        } else {
            px = (frame.width / 2) + frame.hotSpotX;
        }
        
        if (jjMaskedPixel(int(obj.xPos + px), int(obj.yPos + py))) {
            obj.yAcc = 0;
            obj.ySpeed = 0;
            int event = jjEventGet(int(obj.xPos) / 32, int(obj.yPos) / 32); // redundancy-efficiency trade off
            if (event >= OBJECT::REDSPRING && event <= OBJECT::BLUESPRING) {
                for (int i = 1; i < jjObjectCount; i++) {
                    jjOBJ@ spring = jjObjects[i];
                    if (int(spring.eventID) == event && (obj.doesCollide(spring) || (abs(obj.xPos - spring.xPos) < 4 && abs(obj.yPos - spring.yPos) < 8))) {
                        // This was your original speed
                        // obj.ySpeed = 81.5 - int(spring.eventID);
                        // This is the revised version, based on the actual values ...
                        // obj.ySpeed = -(array<float> = {16,24,32})[spring.eventID - OBJECT::REDSPRING];
						obj.ySpeed = -(array<float> = {12,18,24})[spring.eventID - OBJECT::REDSPRING]; // lighter than player = less pressure on spring = less bounce!
                        // ... even thats too fast if ySpeed isn't capped, so you gotta scale it somehow:
                        obj.ySpeed /= 2.5;
                        spring.state = STATE::SPRING;
                        jjSample(spring.xPos, spring.yPos, SOUND::SPRING_SPRING1);
                    }
                }
            }
        } else {
			// Froducish acc (close to player acc)
            // obj.yAcc = 0.125;
			obj.yAcc = 0.09; // demon is lighter than player
        }
    }
    
    void onDraw(jjOBJ@ obj) {
        if (obj.var[7] == 1 && obj.justHit == 0 && obj.freeze == 0) jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::PLAYER, jjPlayers[31].playerID);
        else obj.draw();
//      jjDrawString(obj.xPos, obj.yPos - 32, formatFloat(obj.ySpeed, '', 6, 2), STRING::MEDIUM);
//      jjDrawString(obj.xPos, obj.yPos - 64, formatInt(obj.oldState), STRING::MEDIUM);
    }
}

void bombEdit(jjOBJ@ obj) {
	float dx, dy, distance;
	obj.behave(BEHAVIOR::BOMB);
	if (obj.state == STATE::EXPLODE) {
		for (int i = 1; i < jjObjectCount; i++) {
			jjOBJ@ other = jjObjects[i];
			dx = abs(obj.xPos - other.xPos);
			dy = abs(obj.yPos - other.yPos);
			distance = sqrt(pow(dx, 2) + pow(dy, 2)); // A Perfect Circle...
			if (other.eventID == OBJECT::DESTRUCTSCENERYBOMB && other.state == STATE::SLEEP && distance <= 64) {
				other.state = STATE::KILL;
//				givePlayerPointsForObject(jjLocalPlayers[obj.var[6]], other); -- don't know how to determine this yet :(
			}
		}
	}
}

class brickTrap : jjBEHAVIORINTERFACE {
    void onBehave(jjOBJ@ obj) {
        const uint xTile = uint(obj.xOrg)>>5, yTile = uint(obj.yOrg)>>5;
        int animTileID = jjParameterGet(xTile, yTile, 0, 1) == 0 ? 123 : 124;
        if (obj.state == STATE::START) {
            jjTileSet(4, xTile, yTile, animTileID | TILE::ANIMATED);
            const auto@ tile = jjAnimatedTiles[animTileID];
            const auto@ Frames = tile.getFrames();
            obj.var[1] = Frames[Frames.length-1];
            obj.yPos = obj.yOrg + 16;
            obj.state = STATE::SLEEP;
        }
		if (obj.state == STATE::SLEEP) {
			jjPARTICLE@ particle = jjAddParticle(PARTICLE::ICETRAIL);
			if (particle !is null) {
			particle.xPos = obj.xPos + (jjRandom()%28 - 15);
			particle.yPos = obj.yPos;
			particle.icetrail.color = 72;
			particle.icetrail.colorStop = 80;
			}
			if (jjLocalPlayers[obj.findNearestPlayer(128 * 128)].yPos >= obj.yPos && abs(jjLocalPlayers[obj.findNearestPlayer(128 * 128)].xPos - obj.xPos) <= 64.f) {
				jjOBJ@ brick = jjObjects[jjAddObject(OBJECT::BOUNCEONCE, obj.xPos, obj.yPos + 6)];
				brick.determineCurAnim(ANIM::QUEEN, 4);
				brick.xAcc = 0;
				brick.xSpeed = 0;
				brick.playerHandling = HANDLING::ENEMYBULLET;
				brick.animSpeed = 1;
				jjSample(obj.xPos, obj.yPos + 6, SOUND::COMMON_HOLYFLUT);
				jjTileSet(4, xTile, yTile, obj.var[1]);
				obj.delete();
			}
		}
	}
	void onDraw (jjOBJ@ obj) {
	// don't draw
	}
}

class imp : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.scriptedCollisions = true;
		obj.playerHandling = HANDLING::SPECIAL;
		if (obj.isActive && obj.freeze == 0) {
			obj.var[6] = jjParameterGet(uint16(obj.xOrg / 32), uint16(obj.yOrg / 32), 0, 3);
			obj.var[7] = jjParameterGet(uint16(obj.xOrg / 32), uint16(obj.yOrg / 32), 3, 3);
			obj.counter += 1 + (jjRandom() & 3);
			int arg = obj.counter / ((obj.var[6] + 1) + (obj.var[7] + 1) / 2);
			obj.xPos = obj.xOrg + jjSin(arg * 4) * 32 * (obj.var[6] + 1);
			obj.yPos = obj.yOrg + jjCos(arg * 2) * 24 * (obj.var[7] + 1);
			if (arg & 255 < 128 ^^ obj.direction < 0) {
				jjSample(obj.xPos, obj.yPos, SOUND::COMMON_FOEW1, 10, (obj.direction < 0 ? 30300 : 30000) + (jjRandom() & 0x7FF));
				obj.direction = obj.direction < 0 ? 1 : -1;
			}
			obj.frameID = obj.counter / 6 % jjAnimations[obj.curAnim].frameCount;
			obj.determineCurFrame();
			obj.draw();
		}
		if (obj.state == STATE::KILL) obj.delete();
		if (obj.freeze > 0) {
			if (obj.freeze == 0) {
					obj.unfreeze(0);
					obj.state = obj.oldState;
				}
			else obj.freeze--;
			obj.determineCurFrame();
			obj.draw();
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {

[preview ends here]