Downloads containing CandyGoth.j2as

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Candy GothFeatured Download Violet CLM Single player 8.3 Download file

File preview

const bool MLLESetupSuccessful = MLLE::Setup(); ///@MLLE-Generated
#include "MLLE-Include-1.5.asc" ///@MLLE-Generated
#pragma require "CandyGothDecs.j2t" ///@MLLE-Generated
#pragma require "PezroxV.j2t" ///@MLLE-Generated
#pragma require "CandyGoth.j2l" ///@MLLE-Generated
#pragma require "SkullGem.j2a"
#pragma require "Lubella.j2a"
#pragma offer "beyond_-_creepy.it"

uint FirstTileFrameID;

void onLevelLoad() {
	jjAnimations[jjObjectPresets[OBJECT::REDGEM].curAnim] = jjAnimations[jjAnimSets[ANIM::CUSTOM[0]].load(0, "SkullGem.j2a")];
	Blacken(OBJECT::BAT);
	Blacken(OBJECT::RAVEN);
	Blacken(OBJECT::DEMON);
	jjTexturedBGTexture = TEXTURE::WISETYNESS;
	jjTexturedBGFadePositionY = 0.6;
	jjUseLayer8Speeds = true;
	
	const array<array<int>> tileSpriteDimensions = {
		{0,0, 16,32*5-4, 0,-9},
		{16,0, 16,32*5-4, 0,-9},
		{0, 15*32, 64,32, -32,-32},
		{0, 16*32, 64,32, -32,0},
		{0, 17*32, 9,28, -5,-14},
		{0,0, 96,16, 0,0},
		{1*32, 17*32, 32,32, -16,-16},
		{0, 9*32, 64,64, -32,-32},
		{2*32, 7*32, 128,128, -64,-64},
		{0, 18*32, 64,16, -32,0}
	};
	FirstTileFrameID = jjAnimations[jjAnimSets[ANIM::CUSTOM[1]].allocate(array<uint>={tileSpriteDimensions.length})];
	for (uint i = 0; i < tileSpriteDimensions.length; ++i) {
		jjANIMFRAME@ tileFrame = jjAnimFrames[FirstTileFrameID + i];
		const auto@ dimensions = tileSpriteDimensions[i];
		jjPIXELMAP(dimensions[0],dimensions[1],dimensions[2],dimensions[3], 5).save(tileFrame);
		tileFrame.hotSpotX = dimensions[4];
		tileFrame.hotSpotY = dimensions[5];
	}
	
	///@Event 141=Lower Floor                     |+|Trigger      |Lower|Floor|TriggerID:5
	jjObjectPresets[141].behavior = LoweringFloor;
	jjObjectPresets[141].playerHandling = HANDLING::PARTICLE;
	jjObjectPresets[141].curFrame = FirstTileFrameID;
	jjObjectPresets[141].isBlastable = false;
	
	///@Event 143=Rippling Floor                     |+|Trigger      |Ripple|Floor
	jjObjectPresets[143].behavior = RipplingFloor;
	jjObjectPresets[143].playerHandling = HANDLING::PARTICLE;
	jjObjectPresets[143].curFrame = FirstTileFrameID;
	jjObjectPresets[143].deactivates = false;
	jjObjectPresets[143].isBlastable = false;
	
	if (jjAnimSets[ANIM::BRIDGE] != 0) {
		jjObjectPresets[OBJECT::BRIDGE].behavior = BridgeWrapper;
		jjANIMATION@ bridgeAnim = jjAnimations[jjAnimSets[ANIM::BRIDGE]];
		bridgeAnim.firstFrame = FirstTileFrameID;
		bridgeAnim.frameCount = 2;
	}
	
	///@Event 142=Pentagram                     |+|Goodies      |Pent-|agram|EventID:8|Down:c1
	jjObjectPresets[142].behavior = Pentagram;
	jjObjectPresets[142].playerHandling = HANDLING::PARTICLE;
	jjObjectPresets[142].curFrame = FirstTileFrameID + 2;
	
	///@Event 146=Collapsible Platform                     |+|Trigger      |Collapse|Plat|TriggerID:5
	jjObjectPresets[146].behavior = CollapsiblePlatform;
	jjObjectPresets[146].playerHandling = HANDLING::PARTICLE;
	jjObjectPresets[146].curFrame = FirstTileFrameID + 5;
	jjObjectPresets[146].isBlastable = false;
	
	///@Event 147=Void                     |+|Scenery      |Void
	jjObjectPresets[147].behavior = Void();
	jjObjectPresets[147].playerHandling = HANDLING::SPECIAL;
	jjObjectPresets[147].scriptedCollisions = true;
	jjObjectPresets[147].curFrame = FirstTileFrameID + 6;
	jjObjectPresets[147].isFreezable = false;
	jjObjectPresets[147].triggersTNT = false;
	jjObjectPresets[147].isBlastable = false;
	
	jjAnimations[jjObjectPresets[OBJECT::SPIKEBOLL].curAnim].firstFrame = FirstTileFrameID + 8;
	jjObjectPresets[OBJECT::SPIKEBOLL].behavior = Choker2D;
	if (jjAnimSets[ANIM::ROCK] == 0) jjAnimSets[ANIM::ROCK].load(); //for the sound effect
	jjObjectPresets[OBJECT::SPIKEBOLL].deactivates = false;
	
	///@Event 170=Choker Scenery                     |+|Trigger      |Choke|Scen
	jjObjectPresets[170].behavior = ChokerScenery;
	jjObjectPresets[170].playerHandling = HANDLING::PARTICLE;
	jjObjectPresets[170].determineCurAnim(ANIM::DESTSCEN, 2);
	jjObjectPresets[170].curFrame = jjAnimations[jjObjectPresets[170].curAnim];
	jjObjectPresets[170].deactivates = false; //meh
	
	///@Event 177=Lubella                     |-|Boss      |Lubella
	jjObjectPresets[177].behavior = Lubella();
	jjObjectPresets[177].playerHandling = HANDLING::PARTICLE; //while wating for activation
	jjObjectPresets[177].bulletHandling = HANDLING::DETECTBULLET;
	jjObjectPresets[177].scriptedCollisions = true;
	jjObjectPresets[177].determineCurAnim(ANIM::DESTSCEN, 2);
	jjObjectPresets[177].frameID = 0;
	jjObjectPresets[177].curFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[2]].load(0, "Lubella.j2a")];
	jjObjectPresets[177].isFreezable = true;
	jjObjectPresets[177].triggersTNT = true;
	jjObjectPresets[177].isBlastable = true;
	jjObjectPresets[177].isTarget = true;
	jjObjectPresets[177].energy = 100;
	
	jjObjectPresets[OBJECT::GRASSPLATFORM].behavior = ForceWheel;
	jjObjectPresets[OBJECT::GRASSPLATFORM].playerHandling = HANDLING::PARTICLE;
	jjObjectPresets[OBJECT::GRASSPLATFORM].xAcc = jjObjectPresets[OBJECT::GRASSPLATFORM].yAcc = 0;
	jjObjectPresets[OBJECT::GRASSPLATFORM].deactivates = false;
	
	jjAnimations[jjObjectPresets[OBJECT::SPIKEBOLL3D].curAnim].firstFrame = FirstTileFrameID + 9;
	jjObjectPresets[OBJECT::SPIKEBOLL3D].behavior = Platform3D;
	jjObjectPresets[OBJECT::SPIKEBOLL3D].playerHandling = HANDLING::PARTICLE; //shooting can get it confused, so let's not bother
	
	jjObjectPresets[21].behavior = Teleportal;
	jjObjectPresets[21].playerHandling = HANDLING::PARTICLE;
}
void onLevelReload() {
	MLLE::Palette.apply();
	ImmobilizePlayers = false;
	jjMusicLoad("cute.mod");
}

class Blacken : jjBEHAVIORINTERFACE {
	jjBEHAVIOR behavior;
	Blacken(OBJECT::Object eventID) {
		jjOBJ@ preset = jjObjectPresets[eventID];
		behavior = preset.behavior;
		preset.behavior = this;
		preset.lightType = LIGHT::NORMAL;
		preset.light = 10;
	}
	void onBehave(jjOBJ@ obj) {
		obj.behave(behavior, false);
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, obj.freeze == 0 ? SPRITE::SINGLECOLOR : SPRITE::FROZEN, obj.justHit == 0 ? 0 : 15);
	}
}

void LoweringFloor(jjOBJ@ obj) {
	obj.xPos = obj.xOrg;
	if (obj.state == STATE::START) {
		if (obj.creator == CREATOR::LEVEL) {
			obj.xOrg = obj.xPos -= 15;
			obj.yPos -= 11;
			obj.var[0] = jjAddObject(obj.eventID, obj.xPos + 16, obj.yPos);
		} else {
			obj.deactivates = false;
			obj.curFrame += 1;
		}
		obj.var[1] = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0, 5);
		obj.state = STATE::WAIT;
	} else if (obj.state == STATE::DEACTIVATE) {
		jjDeleteObject(obj.var[0]);
		obj.deactivate();
	} else if (obj.state == STATE::WAIT) {
		if (jjTriggers[obj.var[1]])
			obj.state = STATE::FALL;
		for (int i = 0; i < jjLocalPlayerCount; ++i) {
			jjPLAYER@ play = jjLocalPlayers[i];
			if (play.yPos > obj.yPos - 22 && play.yPos < obj.yPos + 160 && abs(obj.xPos - play.xPos) < 8) {
				play.yPos = obj.yPos - 23;
				play.xPos += 1;
			}
		}
		obj.bePlatform(obj.xPos, obj.yPos);
	} else {
		if (int(obj.yPos) & 31 == 4 && jjMaskedHLine(int(obj.xPos), 16, int(obj.yPos - 4))) {
			obj.clearPlatform();
			obj.delete();
		} else {
			obj.yPos += 1;
			obj.bePlatform(obj.xPos, obj.yPos - 1);
		}
	}
	jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos + 5, obj.curFrame);
}

uint NumberOfPrayers = 0; //static
void Pentagram(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.yPos -= 15;
		obj.state = STATE::WAIT;
		if (jjParameterGet(uint(obj.xOrg) >> 5, (uint(obj.yOrg) >> 5), 8, 1) == 1)
			obj.yPos += 32;
	} else if (obj.state == STATE::DEACTIVATE) {
		obj.deactivate();
	} else if (obj.state == STATE::WAIT) {
		bool beingKneltOn = false;
		for (int i = 0; i < jjLocalPlayerCount; ++i) {
			jjPLAYER@ play = jjLocalPlayers[i];
			if (play.keyDown && play.curAnim - jjAnimSets[play.setID] == RABBIT::DIVE && abs(obj.xPos - play.xPos) < 15 && abs(obj.yPos - play.yPos) < 30) {
				beingKneltOn = true;
				break;
			}
		}
		
		if (beingKneltOn) {
			if (obj.counter < 0)
				obj.counter = 50;
			else if (++obj.counter == 100) {
				NumberOfPrayers += 1;
				bool deleteMe = true;
				jjSample(obj.xPos, obj.yPos, SOUND::DEVILDEVAN_DRAGONFIRE);
				const uint8 eventID = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0, 8);
				if (eventID == OBJECT::BOMB) {
					jjOBJ@ bomb = jjObjects[jjAddObject(OBJECT::BOMB, obj.xPos, obj.yPos - 16, obj.objectID, CREATOR::OBJECT, BombWrapper)];
					bomb.ySpeed = -4;
					bomb.state = STATE::FLY;
					if (jjAnimSets[ANIM::LIZARD] != 0)
						bomb.curAnim = jjAnimSets[ANIM::LIZARD] + 1;
					deleteMe = false;
				} else if (eventID == AREA::TRIGGERZONE) {
					jjTriggers[jjParameterGet(uint(obj.xOrg) >> 5, (uint(obj.yOrg) >> 5) - 1, 0, 5)] = true;
				} else {
					jjOBJ@ spawn = jjObjects[jjAddObject(eventID, obj.xPos, obj.yPos - 16, obj.objectID, CREATOR::OBJECT)];
					if (spawn.behavior == BEHAVIOR::PICKUP) {
						spawn.var[2] = 30;
						spawn.ySpeed = -4;
						spawn.xSpeed = 1;
						spawn.objType = HANDLING::DELAYEDPICKUP;
					} else if (spawn.behavior == BEHAVIOR::SPRING) {
						deleteMe = false;
						obj.state = STATE::FADEOUT; //invisible, but can be deactivated
					} else if (eventID == 21) //teleportal
						deleteMe = false;
				}
				if (deleteMe)
					obj.delete();
				else
					obj.counter = 0;
			} else {
				const uint rand = jjRandom();
				if (rand & 0x8000 != 0) {
					jjAddObject(obj.eventID, obj.xPos - 32 + (rand & 63), obj.yPos - 16 + ((rand >> 6) & 31), obj.objectID, CREATOR::OBJECT, PentagramMagic);
				}
			}
		} else if (NumberOfPrayers < 3) {
			if (--obj.counter < -400 && obj.counter & 7 < 4) {
				for (int i = 0; i < jjLocalPlayerCount; ++i) {
					const jjPLAYER@ play = jjLocalPlayers[i];
					const jjANIMATION@ anim = jjAnimations[jjAnimSets[play.setID] + RABBIT::DIVE];
					const int playerID = play.playerID;
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos - 20, anim.firstFrame + anim.frameCount - 1, 1, SPRITE::TRANSLUCENTPLAYER, playerID, 3,4, playerID);
				}
			}
		}
	}
	jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 1, SPRITE::ALPHAMAP, 29, 4);
	jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame + 1, 1, SPRITE::ALPHAMAP, 29, 3);
}

void PentagramMagic(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.curFrame += 2;
		obj.state = STATE::FLOAT;
	}
	if (jjRandom() & 31 == 0)
		obj.delete();
	else
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos -= 2, obj.curFrame, 1, SPRITE::ALPHAMAP, 29, 3);
}

void BombWrapper(jjOBJ@ obj) {
	if (obj.state == STATE::START)
		obj.playerHandling = HANDLING::PARTICLE;
	else if (obj.ySpeed > 1 && obj.playerHandling == HANDLING::PARTICLE)
		obj.playerHandling = HANDLING::SPECIAL;
	obj.behave(BEHAVIOR::BOMB);
	if (obj.state == STATE::EXPLODE && obj.counter == 1) {
		for (int otherObjectID = jjObjectCount; --otherObjectID != 0;) {
			jjOBJ@ other = jjObjects[otherObjectID];
			if (other.behavior == RipplingFloor && abs(obj.xPos - 8 - other.xPos) < 16)
				other.state = STATE::BOUNCE;
		}
	}
}

void BridgeWrapper(jjOBJ@ obj) {
	obj.xPos += 1;
	obj.behavior = BEHAVIOR::BRIDGE;
	obj.behave();
}

void RipplingFloor(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		if (obj.creator == CREATOR::LEVEL) {
			obj.xPos -= 15;
		} else {
			obj.var[0] = obj.creatorID;
			if (int(obj.xPos) & 31 == 16)
				obj.curFrame += 1;
		}
		obj.yPos -= 11;
		if (!jjMaskedPixel(int(obj.xPos) + 16, int(obj.yPos)) || jjTileGet(4, (int(obj.xPos) + 16) >> 5, int(obj.yOrg) >> 5) == 552)
			obj.var[1] = jjAddObject(obj.eventID, obj.xPos + 16, obj.yOrg, obj.objectID);
		obj.state = STATE::WAIT;
	} else if (obj.state == STATE::DEACTIVATE) {
		//jjDeleteObject(obj.var[0]);
		//obj.deactivate();
	} else {
		const auto lastY = obj.yPos;
		if (obj.state == STATE::BOUNCE) {
			obj.yPos = obj.yOrg - 11 + jjSin(obj.counter += 16) * 26;
			if (obj.counter == 1024) {
				obj.counter = 0;
				obj.state = STATE::WAIT;
			} else if (obj.counter == 48) {
				for (uint i = 0 ; i < 2; ++i)
					if (obj.var[i] != 0) {
						jjOBJ@ other = jjObjects[obj.var[i]];
						if (other.state == STATE::WAIT)
							other.state = STATE::BOUNCE;
					}
			} else if (obj.counter == 768 - 64) { //highest point
				for (int i = 0; i < jjLocalPlayerCount; ++i) {
					jjPLAYER@ play = jjLocalPlayers[i];
					if (play.platform == obj.objectID) {
						const auto realCurFrame = obj.curFrame;
						obj.ySpeed = -25;
						obj.special = CREATOR::PLAYER;
						obj.state = STATE::SPRING;
						obj.behave(BEHAVIOR::SPRING, false);
						obj.state = STATE::BOUNCE;
						obj.curFrame = realCurFrame;
					}
				}
			}
		}
		for (int i = 0; i < jjLocalPlayerCount; ++i) {
			jjPLAYER@ play = jjLocalPlayers[i];
			if (play.yPos > obj.yPos - 22 && play.yPos < obj.yPos + 160 && abs(obj.xPos - play.xPos) < 8) {
				play.yPos = obj.yPos - 23;
				play.xPos += 1;
			}
		}
		obj.bePlatform(obj.xPos, lastY);
	}
	jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos + 5, obj.curFrame, 1, SPRITE::NORMAL,0, 5);
}

void CollapsiblePlatform(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.xPos -= 15;
		obj.yPos -= 11;
		obj.state = STATE::WAIT;
		obj.counter = 30;
		if (obj.creator == CREATOR::LEVEL)
			obj.var[0] = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0, 5);
	} else if (obj.state == STATE::DEACTIVATE) {
		obj.deactivate();
	} else {
		if (obj.counter >= 6) {
			obj.bePlatform(obj.xPos, obj.yPos);
			if (obj.counter == 30) {
				if (obj.var[0] == 0)
					for (int i = 0; i < jjLocalPlayerCount; ++i) {
						const jjPLAYER@ play = jjLocalPlayers[i];
						if (play.platform == obj.objectID) {
							obj.counter = 29;
							jjSample(obj.xPos, obj.yPos, SOUND::COMMON_COLLAPS);
							break;
						}
					}
				else if (jjTriggers[obj.var[0]])
					obj.counter = 29;
			}
		} else
			obj.clearPlatform();
		for (int x = 0; x < 6; ++x)
			for (int y = 0; y < 4; ++y) {
				const int position = x + y*6;
				const uint16 tileID =  1 + (x>>1) + (y>>1)*10;
				const TILE::Quadrant quadrant = TILE::Quadrant((x&1) + ((y&1)<<1));
				const float xPos = obj.xPos + (x<<4), yPos = obj.yPos + (y<<4) - 4;
				if (position < obj.counter)
					jjDrawTile(xPos, yPos, tileID, quadrant);
				else if (position == obj.counter && jjGameTicks & 1 == 0) {
					jjPARTICLE@ fragment = jjAddParticle(PARTICLE::TILE);
					if (fragment !is null) {
						fragment.xPos = xPos;
						fragment.yPos = yPos;
						fragment.tile.tileID = tileID;
						fragment.tile.quadrant = quadrant;
						fragment.xSpeed = (x - 2.5) / 6;
					}
				}
			}
		if (obj.counter < 30 && jjGameTicks & 1 == 1 && --obj.counter < 0)
			obj.delete();
	}
}

class Void : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		if (obj.state == STATE::START) {
			obj.state = STATE::FADEIN;
			obj.xAcc = 0; //scale
			obj.special = jjRandom();
			if (obj.creatorType == CREATOR::LEVEL) {
				int childID = 0;
				for (int x = -1; x <= 1; x += 2)
					for (int y = -1; y <= 1; y += 2)
						obj.var[childID++] = jjAddObject(obj.eventID, obj.xOrg + (x<<3), obj.yOrg + (y<<3));
			}
			const float scale = (jjTileGet(4, int(obj.xOrg)>>5, int(obj.yOrg)>>5) != 592) ? 1.0f : 2.5f;
			obj.xPos += (int(jjRandom() & 7) - 3.5) * scale;
			obj.yPos += (int(jjRandom() & 7) - 3.5) * scale;
		} else if (obj.state == STATE::DEACTIVATE) {
			obj.deactivate();
		} else if (obj.state == STATE::FADEOUT) {
			if (obj.xAcc > 0)
				obj.xAcc -= 0.125;
			else if (obj.xSpeed == 0 && obj.ySpeed == 0)
				obj.state = STATE::FADEIN;
			else
				obj.delete();
		} else if (obj.state == STATE::FADEIN) {
			if (obj.counter != 0)
				--obj.counter;
			else if (obj.xAcc < 1)
				obj.xAcc += 0.125;
			else {
				obj.state = STATE::ROTATE;
				obj.playerHandling = HANDLING::SPECIAL;
			}
		} else if (obj.state == STATE::ROTATE) {
			obj.special += 3;
		}
		
		if (obj.xAcc > 0) {
			jjDrawResizedSpriteFromCurFrame(obj.xPos + jjSin(obj.special) * 3, obj.yPos + jjCos(obj.special) * 3, obj.curFrame, obj.xAcc,obj.xAcc, SPRITE::ALPHAMAP, 0, 2);
			if (obj.xSpeed != 0 || obj.ySpeed != 0) {
				obj.xPos += obj.xSpeed;
				obj.yPos += obj.ySpeed;
				if (++obj.age > 1100)
					obj.delete();
			}
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (bullet is null) {
			player.hurt();
		} else {
			obj.state = STATE::FADEOUT;
			obj.playerHandling = HANDLING::PARTICLE;
			obj.counter = 110 + (3 - jjDifficulty) * 30;
			if (bullet.freeze != 0) //ice
				obj.counter += 55;
		}
		return true;
	}
}

void Choker2D(jjOBJ@ obj) {
	int angle;
	const SPRITE::Mode mode = (obj.freeze == 0) ? SPRITE::NORMAL : SPRITE::FROZEN;
	if (obj.state == STATE::KILL) {
		auto xSpeed = obj.xSpeed * 2, ySpeed = obj.ySpeed * 2;
		if (xSpeed >= 0) {
			if (xSpeed < 1.5)
				xSpeed = 1.5;
		} else if (xSpeed > -1.5)
			xSpeed = -1.5;
		obj.behave(BEHAVIOR::PLATFORM, false); //make chain fragments
		obj.energy = 0;
		obj.state = STATE::FLY;
		obj.xSpeed = xSpeed;
		obj.ySpeed = ySpeed;
		obj.playerHandling = HANDLING::SPECIAL;
		obj.behavior = Choker2D; //still!
		return;
	} else if (obj.energy <= 0) {
		if (obj.yPos > 2050) { //fell out of the level
			obj.delete();
		} else {
			const auto lastX = abs(obj.xSpeed);
			const auto lastY = obj.ySpeed;
			obj.behave(BEHAVIOR::ROTATINGROCK, false);
			if (abs(obj.xSpeed) < lastX) {
				obj.xSpeed = (obj.xSpeed >= 0) ? lastX : -lastX;
			}
			if (jjEventGet(uint(obj.xPos) >> 5, uint(obj.yPos) >> 5) == AREA::PATH) {
				if ((obj.xSpeed >= 0) != (jjParameterGet(uint(obj.xPos) >> 5, uint(obj.yPos) >> 5, 0,1) == 0))
					obj.xSpeed = -obj.xSpeed;
			}
			if (obj.state == STATE::DONE) {
				obj.ySpeed = lastY;
				obj.state = STATE::FLY;
			}
			angle = obj.var[0] / 2;
			obj.deactivates = false;
	
			//this would work fine if available for ::PLATFORM chokers too, except that the level layout offers no such opportunities.
			for (int otherObjectID = jjObjectCount; --otherObjectID != 0;) {
				jjOBJ@ other = jjObjects[otherObjectID];
				if (other.behavior == ChokerScenery && other.state == STATE::IDLE && other.doesCollide(obj))
					other.state = STATE::KILL;
			}
		}
	} else {
		const auto lastX = obj.xPos, lastY = obj.yPos;
		obj.behave(BEHAVIOR::PLATFORM, false);
		
		const int chainFrameID = jjAnimations[obj.killAnim];
		const int radadd=jjAnimFrames[chainFrameID].width-1;
		const int angleadd = -obj.var[5];
		int radius = 0;
		angle = obj.var[4] - angleadd * obj.var[2];

		for (int n = 0; n < obj.var[2]; ++n) {
			jjDrawSpriteFromCurFrame(
				obj.xOrg + radius * jjSin(angle),
				obj.yOrg + radius * jjCos(angle),
				chainFrameID, 1,
				mode
			);
			angle += angleadd;
			radius += radadd;
		};
		
		radius += radadd * 2;

		obj.xPos = obj.xOrg + radius * jjSin(angle);
		obj.yPos = obj.yOrg + radius * jjCos(angle);
		obj.xSpeed = obj.xPos - lastX;
		obj.ySpeed = obj.yPos - lastY;
	}
	jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, -angle << 1, 1,1, mode);
	jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame - 1, 1, mode);
}

void ChokerScenery(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.state = STATE::IDLE;
		obj.var[0] = int(obj.xPos) >> 5;
		obj.var[1] = int(obj.yPos) >> 5;
		obj.var[2] = jjTileGet(4, obj.var[0], obj.var[1]);
		const auto@ frames = jjTiles[obj.var[2]].getFrames();
		obj.var[3] = frames.length <= 1 ? 0 : frames[1];
		obj.var[4] = frames[(frames.length > 2) ? (frames.length - 1) : 0];
	} else if (obj.state == STATE::KILL) {
		jjAddParticleTileExplosion(obj.var[0], obj.var[1], obj.var[4], false);
		jjTileSet(4, obj.var[0], obj.var[1], obj.var[3]);
		jjSample(obj.xPos, obj.yPos, SOUND::COMMON_DAMPED1);
		obj.state = STATE::DONE;
		obj.deactivates = false;
	} else if (obj.state == STATE::DEACTIVATE) {
		if (jjDeactivatingBecauseOfDeath)
			jjTileSet(4, obj.var[0], obj.var[1], obj.var[2]);
		obj.deactivate();
	}
}

int DesiredModSpeed;
class Lubella : jjBEHAVIORINTERFACE {
	jjPLAYER@ nearestPlayer(const jjOBJ@ obj) const {
		return jjPlayers[obj.findNearestPlayer(0x7FFFFFFF)];
	}
	void onBehave(jjOBJ@ obj) {
		if (obj.justHit == 0 && obj.doesHurt > 1) {
			obj.justHit = (obj.doesHurt -= 2);
		}
		//breathing counter:
		++obj.age;
		if (obj.state != STATE::FREEZE) {
			if (obj.state == STATE::ATTACK || obj.state == STATE::FIRE) {
				if (obj.frameID < 15) {
					obj.frameID += 1;
					obj.yPos -= 1;
					obj.yOrg -= 1;
				}
			} else {
				if (obj.frameID > 0) {
					obj.frameID -= 1;
					obj.yPos += 1;
					obj.yOrg += 1;
				}
			}
		}
		
		if (obj.energy < 100 && jjGetModSpeed() != DesiredModSpeed)
			jjSetModSpeed(DesiredModSpeed);
		
		switch (obj.state) {
		case STATE::START:
			obj.state = STATE::DELAYEDSTART;
			obj.var[1] = 75; //next energy point to descend at
			obj.yPos += 200;
			return;
		case STATE::DELAYEDSTART:
			for (int i = 0; i < jjLocalPlayerCount; ++i) {
				jjPLAYER@ localPlayer = jjLocalPlayers[i];
				if (localPlayer.bossActivated) {
					localPlayer.boss = obj.objectID;
					obj.state = STATE::FADEIN;
					break;
				}
			}
			return;
		case STATE::DEACTIVATE:
			obj.deactivate(); //sure
			return;
		case STATE::FREEZE:
			if (--obj.freeze == 0) {
				obj.unfreeze(0);
				obj.state = obj.oldState;
			}
			return;
		case STATE::FADEIN:
			obj.yPos = obj.yOrg + 200 - jjSin(obj.counter += 4) * 256;
			if (obj.counter >= 256) {
				obj.justHit = 3;
				obj.var[0] = 1;
				if (obj.counter == 360) {
					obj.playerHandling = HANDLING::SPECIAL;
					obj.state = STATE::WALK;
					obj.doesHurt = 10;
					obj.counter = 0;
					jjSample(obj.xPos, obj.yPos, SOUND::PICKUPS_BOING_CHECK);
					for (int i = 0; i < jjLocalPlayerCount; ++i) {
						jjPLAYER@ localPlayer = jjLocalPlayers[i];
						if (localPlayer.doesCollide(obj)) {
							localPlayer.hurt();
							localPlayer.yPos = obj.yPos - 25;
							localPlayer.ballTime = 50;
						}
					}
				}
			}
			return;
		case STATE::FADEOUT:
			obj.yPos = obj.yOrg + 200 - jjSin(obj.counter -= 4) * 256;
			if (obj.counter <= 256) {
				obj.var[0] = 0;
				obj.justHit = 4;
				if (obj.counter == 0) {
					obj.state = STATE::DUCK;
					obj.playerHandling = HANDLING::PARTICLE;
				}
			}
			return;
		case STATE::DUCK:
			if (obj.energy <= 0) {
				obj.state = STATE::DONE;
				jjNxt();
			} else if (++obj.counter == 700) {
				obj.counter = 0;
				obj.state = STATE::FADEIN;
			} else {
				if (obj.var[1] != 50 && obj.counter < 80 && obj.counter & 3 == 1)
					jjAddObject(OBJECT::BAT, obj.xPos - 32 + ((obj.counter & 15) << 4), obj.yPos + ((obj.counter >> 4) << 5), (obj.special < 0) ? 2 : 0, CREATOR::OBJECT, BossBat);
				if (obj.var[1] != 25 && obj.counter & 63 == 5) {
					const jjPLAYER@ target = nearestPlayer(obj);
					jjAddObject(OBJECT::SPIKEBOLL, target.xPos, target.cameraY - 130, obj.objectID, CREATOR::OBJECT, BossChoker);
				}
			}
			break;
		case STATE::WALK:
			if (obj.energy <= obj.var[1]) { //regular (health-based) lowering intervals
				obj.var[1] = obj.var[1] - 25;
				obj.counter = 360;
				obj.state = STATE::FADEOUT;
				DesiredModSpeed -= 1;
			} else if (++obj.counter >= 165 - (jjDifficulty * 15)) { //attack patterns
				obj.counter = 0;
				obj.counterEnd = 0;
				if ((obj.var[2] = obj.var[2] ^ 1) == 1) {
					obj.state = STATE::ATTACK;
					if ((obj.var[3] = (obj.var[3] + 1) % 3) == 2) //attack both sides
						obj.var[4] = obj.var[6] = 1;
					else { //attack player only
						const jjPLAYER@ play = nearestPlayer(obj);
						obj.var[4] = (play.xPos < obj.xPos) ? 1 : 0;
						obj.var[6] = obj.var[4] ^ 1;
					}
				} else {
					obj.state = STATE::FIRE;
				}
			}
			break;
		case STATE::ATTACK:
			if (obj.counterEnd < 92)
				obj.counterEnd += 1;
			else if (++obj.counter == 320) {
				obj.state = STATE::WALK;
				obj.counter = 0;
			} else if (obj.counter > 40 && obj.counter & 15 == 0) {
				const auto headYPos = obj.yPos - 66 - 92 + jjSin(obj.age << 2) * 3;
				for (int hornDir = -1; hornDir <= 1; hornDir += 2)
					if (obj.var[5 + hornDir] == 1)
						for (int dec = 0; dec < 8; ++dec) {
							jjOBJ@ bullet = jjObjects[jjAddObject(OBJECT::FIREBALLBULLETPU, obj.xPos + (38 + dec * 8) * hornDir, headYPos - dec * 7, obj.objectID)];
							bullet.xPos = bullet.xOrg;
							bullet.ySpeed = 5;
							bullet.xSpeed = (dec + 1) * 0.7 * hornDir;
							bullet.xAcc = bullet.yAcc = 0;
							bullet.playerHandling = HANDLING::ENEMYBULLET;
							bullet.animSpeed = (jjDifficulty != 0) ? 2 : 1;
						}
			}
			return;
		case STATE::FIRE:
			if (++obj.counter == 520) {
				obj.state = STATE::WALK;
				obj.counter = 0;
			} else if ((obj.counterEnd += (1 << jjDifficulty)) == 240) {
				const jjPLAYER@ play = nearestPlayer(obj);
				const auto spawnY = obj.yPos - 50;
				const auto angle = atan2(play.yPos - spawnY, play.xPos - obj.xPos);
				const float xSpeed = cos(angle) * 3;
				const float ySpeed = sin(angle) * 3;
				jjOBJ@ voidHub = jjObjects[jjAddObject(147, obj.xPos, spawnY, obj.objectID, CREATOR::LEVEL)];
				voidHub.xSpeed = xSpeed;
				voidHub.ySpeed = ySpeed;
				for (int i = 0; i < 4; ++i) {
					jjOBJ@ voidChild = jjObjects[voidHub.var[i]];
					voidChild.xSpeed = xSpeed;
					voidChild.ySpeed = ySpeed;
				}
			}
			return;
		case STATE::DONE:
			obj.yPos += 1;
			return;
		}
		
		//if break instead of return, track you horizontally/adjust hair angle:
		const jjPLAYER@ play = nearestPlayer(obj);
		if (play.xPos > obj.xPos + 70) {
			obj.xPos += 0.5;
			if (obj.special > -9)
				obj.special -= 1;
		} else if (play.xPos < obj.xPos + -70) {
			obj.xPos -= 0.5;
			if (obj.special < 9)
				obj.special += 1;
		} else if (obj.special > 0)
			obj.special -= 1;
		else if (obj.special < 0)
			obj.special += 1;
	}
	void onDraw(jjOBJ@ obj) {
		const bool isFrozen = obj.freeze != 0;
		const SPRITE::Mode mode = !isFrozen ? SPRITE::NORMAL : SPRITE::FROZEN;
		float yBreathing = jjSin(obj.age << 2);
		const auto titsYPos = obj.yPos - obj.justHit - yBreathing * 4;
		if (isFrozen) yBreathing = 0;
		const auto hairYPos = obj.yPos - 136 + (!isFrozen ? (jjSin((obj.age << 2) - 96) * 6) : 0);
		for (int hairFrameID = 3; hairFrameID <= 8; hairFrameID += 5)
			jjDrawRotatedSpriteFromCurFrame(obj.xPos, hairYPos, obj.curFrame + hairFrameID, obj.special,1,1, mode,0, 5); //hair
		const auto state = (obj.state != STATE::FREEZE) ? obj.state : obj.oldState;
		const bool summoningArms = state == STATE::FIRE && obj.counter > 16;
		const auto armY = obj.yPos - 30;
		for (int armDir = -1; armDir <= 1; armDir += 2) {
			const bool hornyArm = state == STATE::ATTACK && obj.var[5 + armDir] == 1;
			const int armAngle =
				!summoningArms ?
					!hornyArm ?
						(16 + int(yBreathing * 16) /*+ (!isFrozen ? (obj.justHit << 1) : 0)*/) :
					((50 - int(abs(int(obj.counterEnd) - 50))) * 7 + (obj.counterEnd == 92 ? int(yBreathing * 6) : 0)) :
				(64 + int(yBreathing * 7));
			const int armFrame = obj.curFrame + (!summoningArms && !hornyArm ? 2 : 9);
			const auto armX = obj.xPos + 28 * armDir;
			jjDrawRotatedSpriteFromCurFrame(armX, armY, armFrame, armDir * armAngle, armDir,1, mode,0, 5);
			if (summoningArms || hornyArm) {
				const float sin = jjSin(armAngle);
				const float cos = jjCos(armAngle);
				jjDrawRotatedSpriteFromCurFrame(armX + (cos*23 + sin*53) * armDir, armY + (sin*-23 + cos*53), armFrame + 1, -armDir * (summoningArms ? ((armAngle * 3) - int(abs(int(obj.counterEnd) - 128))) : (armAngle >> 1)), -armDir,1, mode,0, 4);
			}
		}
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame + 1, 0, mode,0, 5); //torso
		const auto headYPos = obj.yPos - 66 + yBreathing * 3;
		const bool frowning = obj.justHit != 0 && !isFrozen; //maybe
		if (!frowning) {
			jjDrawSpriteFromCurFrame(obj.xPos, headYPos, obj.curFrame + 5, 0, mode,0, 5); //eye holes
			if (!isFrozen) {
				const auto eyeYPos = headYPos - 35;
				const jjPLAYER@ play = nearestPlayer(obj);
				for (int i = -18; i <= 18; i += 36) {
					const auto eyeXPos = obj.xPos + i;
					const auto eyeAngle = atan2(play.yPos - eyeYPos, play.xPos - eyeXPos);
					jjDrawSpriteFromCurFrame(eyeXPos + cos(eyeAngle) * 6, eyeYPos + sin(eyeAngle) * 6, obj.curFrame + 6, i, SPRITE::NORMAL,0, 5);
				}
			}
		}
		jjDrawSpriteFromCurFrame(obj.xPos, headYPos, obj.curFrame + 4, 0, mode,0, 5); //head
		if (frowning)
			jjDrawSpriteFromCurFrame(obj.xPos, headYPos, obj.curFrame + 7, 0, mode,0, 5); //grouchy face
		if (state == STATE::ATTACK && obj.counterEnd > 40) {
			for (int hornDir = -1; hornDir <= 1; hornDir += 2)
				if (obj.var[5 + hornDir] == 1)
					for (int dec = 0; dec < 8; ++dec) {
						const float scale = 1.35 - dec * 0.05;
						jjDrawRotatedSpriteFromCurFrame(obj.xPos + (38 + dec * 8) * hornDir, headYPos - 92 - dec * 7, obj.curFrame + 11, (30 + dec * 7) * hornDir, hornDir * scale, scale, !isFrozen ? SPRITE::BRIGHTNESS : SPRITE::FROZEN, 128 - (dec << 3), 5);
					}
		}
		const bool emerged = obj.var[0] == 1;
		if (emerged)
			jjDrawSpriteFromCurFrame(obj.xPos, titsYPos + 5, obj.curFrame, 1, SPRITE::SHADOW,0, 4); //tits
		jjDrawSpriteFromCurFrame(obj.xPos, titsYPos, obj.curFrame, 1, mode,0, emerged ? 3 : 5); //tits
		if (summoningArms)
			for (int half = 0; half < 2; ++half)
				jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos - 45, FirstTileFrameID + 2 + half, jjGameTicks << 1, 1,1, SPRITE::ALPHAMAP, 0, 3);
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (bullet is null) {
			if (player.ballTime < 30) {
				player.ballTime = 35;
				player.xSpeed = (player.xPos - obj.xPos) / 7;
				player.ySpeed = (player.yPos - obj.yPos) / 6;
				player.buttstomp = 121;
				obj.justHit = 8;
				jjSample(obj.xPos, obj.yPos, SOUND::PICKUPS_BOING_CHECK);
			}
		} else {
			if (obj.justHit == 0) {
				if (obj.freeze != 0) {
					obj.freeze = 0;
					obj.unfreeze(1);
					obj.state = obj.oldState;
					force += 3;
				} else {
					if (jjIsTSF)
						jjSample(obj.xPos, obj.yPos - 70, SOUND::Sample(SOUND::LORISOUNDS_HURT0 + (jjRandom() & 7)), 63, 25000);
					else
						jjSample(obj.xPos, obj.yPos - 70, SOUND::Sample(SOUND::JAZZSOUNDS_HEY1 + (jjRandom() & 3)), 63, 17500);
				}
				obj.justHit = 7;
				obj.energy -= force;
				obj.doesHurt = 6;
			}
			bullet.state = STATE::EXPLODE; //no fireballs in level
		}
		return true;
	}
}

void BossBat(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.direction = obj.creatorID - 1;
		obj.determineCurAnim(ANIM::BAT, 0);
		obj.state = STATE::WAKE;
		//obj.points = 0;
	} if (obj.state == STATE::WAKE) {
		if (++obj.counter < 200) {
			obj.xPos += obj.direction;
			obj.yPos -= 2.5;
			obj.frameID = obj.counter >> 3;
			obj.determineCurFrame();
		} else
			obj.state = STATE::FLY;
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::SINGLECOLOR, 0, 3);
	} else {
		obj.xOrg = jjPlayers[obj.var[0]].xPos;
		obj.yOrg = jjPlayers[obj.var[0]].yPos;
		obj.behave(jjObjectPresets[OBJECT::BAT].behavior);
	}
}

void BossChoker(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.energy = 0;
		obj.bulletHandling = HANDLING::DESTROYBULLET;
		obj.isFreezable = true; //this is fine
		obj.isBlastable = false;
		obj.state = STATE::FALL;
		obj.ySpeed = 3 + jjDifficulty;
		obj.curFrame = jjAnimations[obj.curAnim];
	} else if (obj.freeze != 0) {
		if (--obj.freeze == 0) {
			obj.unfreeze(0);
			obj.state = obj.oldState;
		}
	} else if (++obj.age >= 200) {
		obj.deactivate();
	} else
		obj.yPos += obj.ySpeed;
	jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, -(obj.objectID + obj.age) << 3, 1,1, obj.freeze == 0 ? SPRITE::NORMAL : SPRITE::FROZEN, 0, 3);
	jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame - 1, 1, obj.freeze == 0 ? SPRITE::NORMAL : SPRITE::FROZEN, 0, 3);
}

///@Event 211=Force Wheel                  |-|Platform  |Force  |Wheel   |Sync:2|Speed:-6|Length:4|Swing:{Circle,Pendulum}1|Angle:{R,D,UR,DR}2
void ForceWheel(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.xAcc = obj.xOrg;
		obj.yAcc = obj.yOrg;
		const int angle = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 13, 2);
		if (angle != 1)
			obj.var[9] = 1;
		if (angle == 2)
			obj.var[10] = -1;
		else if (angle & 1 == 1)
			obj.var[10] = 1;
		obj.doesHurt = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0, 2);
	} else if (obj.state == STATE::DEACTIVATE) {
		if (obj.xAcc != 0 && obj.yAcc != 0) {
			obj.xOrg = obj.xAcc;
			obj.yOrg = obj.yAcc;
		}
	}
	obj.behave(BEHAVIOR::PLATFORM);
	bool stoodOn = false;
	for (int i = 0; i < jjLocalPlayerCount; ++i) {
		const jjPLAYER@ play = jjLocalPlayers[i];
		if (play.platform == obj.objectID) {
			stoodOn = true;
			if (play.spriteMode == SPRITE::INVISIBLE && (play.blink == 0 || (jjRenderFrame & 4) == 4))
				jjDrawSprite(play.xPos, play.yPos, play.setID, play.keyRun ? RABBIT::RUN3 : RABBIT::RUN1, jjGameTicks >> 2, play.direction, SPRITE::PLAYER, play.playerID, 3);
		}
	}
	if (!stoodOn && obj.doesHurt != 0) {
		if (obj.xOrg > obj.xAcc + 0.5) obj.xOrg -= 1;
		else if (obj.xOrg < obj.xAcc - 0.5) obj.xOrg += 1;
		if (obj.yOrg > obj.yAcc + 0.5) obj.yOrg -= 1;
		else if (obj.yOrg < obj.yAcc - 0.5) obj.yOrg += 1;
	}
}
void onPlayer(jjPLAYER@ play) {
	if (ImmobilizePlayers) {
		//play.spriteMode = SPRITE::INVISIBLE;
		play.keyFire = play.keyJump = play.keyRun = play.keySelect = play.keyLeft = play.keyRight = play.keyUp = play.keyDown = false;
		play.xSpeed = 0;
		play.invincibility = -2;
		return;
	} //else play.spriteMode = SPRITE::PLAYER;
	jjOBJ@ obj = jjObjects[play.platform];
	if (obj.behavior == ForceWheel) {
		int negacc;
				if (obj.counter>256*511) {
					negacc=obj.counter/256-1024;
				} else {
					negacc=obj.counter/256;
				}
				if ((obj.var[3]>32) && (negacc>0)) {
					negacc+=32;
				} else if ((obj.var[3]<-32) && (negacc<0)) {
					negacc-=32;
				}
		const int maxChange = int(abs(negacc)) + 34;
		play.spriteMode = SPRITE::INVISIBLE;
		play.idle = 0;
		if (play.keyRight) {
			obj.var[3] = obj.var[3] + maxChange;
			if (play.direction == 1)
				play.keyRight = false;
		} else if (play.keyLeft) {
			obj.var[3] = obj.var[3] - maxChange;
			if (play.direction == -1)
				play.keyLeft = false;
		} else
			play.spriteMode = SPRITE::PLAYER;
		obj.var[8] = obj.var[8] + maxChange - 34;
		while (obj.var[8] >= 256) {
			obj.var[8] = obj.var[8] - 256;
			const int
				xTarget = int(obj.xOrg) + play.direction * obj.var[9],
				yTarget = int(obj.yOrg) + play.direction * obj.var[10];
			if (!jjMaskedPixel(xTarget, yTarget) && jjEventGet(xTarget>>5, yTarget>>5) != AREA::STOPENEMY) {
				obj.xOrg = xTarget;
				obj.yOrg = yTarget;
			}
		}
		play.xSpeed = 0;
	} else
		play.spriteMode = SPRITE::PLAYER;
		
	if (play.boss != 0 && jjSubscreenHeight >= 320)
		play.cameraFreeze(387*32 + 16, 360, true, true);
	else
		play.cameraUnfreeze();
}

void Platform3D(jjOBJ@ obj) {
	if (obj.state == STATE::START && jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 1, 1) == 1)
		obj.special = 512;
	obj.behave(BEHAVIOR::SPIKEBOLL3D);
	if (obj.curFrame != 0 && obj.state != STATE::DEACTIVATE) {
		obj.bePlatform(obj.xPos, obj.yPos);
		if (obj.counterEnd < 50) {
			++obj.counterEnd;
			--obj.counter;
			obj.var[4] = obj.special - obj.var[5];
		}
	} else {
		obj.clearPlatform();
		if (obj.counterEnd != 0) {
			obj.counterEnd = 0;
			obj.special = obj.special ^ 512;
		}
	}
}

bool ImmobilizePlayers = false;
class TileFragment {
	uint16 tileID;
	uint8 quadrant;
	int xOrg, yOrg, angle;
	TileFragment(){}
	TileFragment(uint16 i, uint8 q, int x, int y, int a){ tileID = i; quadrant = q; xOrg = x; yOrg = y; angle = a; }
}
array<TileFragment> TileFragments;
void GatherTileFragments() {
	const int screenWidthTiles = (jjSubscreenWidth + 31) >> 5;
	const int screenHeightTiles = (jjSubscreenHeight + 31) >> 5;
	for (int i = 0; i < jjLocalPlayerCount; ++i) {
		const jjPLAYER@ play = jjLocalPlayers[i];
		const int cameraX = int(play.cameraX), cameraY = int(play.cameraY);
		for (int layerID = 6; layerID >= 3; --layerID) {
			const jjLAYER@ layer = jjLayers[layerID];
			int layerCameraX, layerCameraY;
			if (layerID != 6) {
				layerCameraX = cameraX & ~31;
				layerCameraY = cameraY & ~31;
			} else {
				layerCameraX = int(layer.getXPosition(play));
				layerCameraY = int(layer.getYPosition(play));
			}
			const int layerXOrg = layerCameraX >> 5, layerYOrg = layerCameraY >> 5;
			if (layerID == 6) {
				layerCameraX = cameraX - ((layerCameraX & 31));
				layerCameraY = cameraY - ((layerCameraY & 31));
			}
			for (int x = 0; x <= screenWidthTiles; ++x)
				for (int y = 0; y <= screenHeightTiles; ++y) {
					const uint16 tileID = jjTileGet(layerID, layerXOrg + x, layerYOrg + y);
					if (tileID != 0)
						for (int q = 0; q < 4; ++q)
							TileFragments.insertLast(TileFragment(tileID, q, layerCameraX + (x << 5) + ((q & 1) << 4), layerCameraY + (y << 5) + ((q & 2) << 3), jjRandom()));
				}
			}
	}
}
void Teleportal(jjOBJ@ obj) {
	if (obj.counter == 0) {
		ImmobilizePlayers = true;
		GatherTileFragments();
		for (int layerID = 6; layerID >= 3; --layerID)
			jjLayerHasTiles[layerID] = false;
		//jjLayers[6].spriteMode = SPRITE::BLEND_NORMAL;
		jjSamplePriority(SOUND::COMMON_TELPORT1);
	} else if (obj.counter == 256) {
		const uint8 warpID = jjParameterGet(uint(obj.xOrg) >> 5, (uint(obj.yOrg) >> 5) - 1, 0, 8);
		jjSetLayerXSpeed(7, 0, false);
		jjSetLayerXSpeed(8, 0, false);
		jjSetLayerYSpeed(8, 0, false);
		for (int i = 0; i < jjLocalPlayerCount; ++i)
			jjLocalPlayers[i].warpToID(warpID, true);
		jjSetLayerXSpeed(7, 0.1963043, false);
		jjSetLayerXSpeed(8, 0.6, false);
		jjSetLayerYSpeed(8, 0.6, false);
		TileFragments.resize(0);
	} else if (obj.counter == 258) {
		GatherTileFragments();
	} else if (obj.counter >= 516) {
		for (int layerID = 6; layerID >= 3; --layerID)
			jjLayerHasTiles[layerID] = true;
		//jjLayers[6].spriteMode = SPRITE::NORMAL;
		ImmobilizePlayers = false;
		TileFragments.resize(0);
		jjSamplePriority(SOUND::COMMON_TELPORT2);
		obj.delete();
	} else {
		const int radius = ((obj.counter < 256) ? obj.counter * 4 : (obj.counter < 510) ? (510 - obj.counter) * 4 : (517 - obj.counter) / 2);
		//jjLayers[6].spriteParam = (radius / 4 < 256) ? (255 - radius / 4) : 0;
		for (uint i = 0; i < TileFragments.length; ++i) {
			TileFragment@ fragment = TileFragments[i];
			fragment.angle += 4;
			jjDrawTile(fragment.xOrg + jjSin(fragment.angle) * radius, fragment.yOrg + jjCos(fragment.angle) * radius, fragment.tileID, TILE::Quadrant(fragment.quadrant));
		}
	}
	obj.counter += (obj.counter < 500) ? 2 : 1;
}

void onFunction0(jjPLAYER@ play, bool left) {
	if (abs(play.xSpeed) > 3 && ((play.xSpeed < 0) == left))
		play.ySpeed = 5;
}

void onFunction1(jjPLAYER@ play) {
	if (!play.bossActivated) {
		play.activateBoss();
		if (jjMusicLoad("beyond_-_creepy.it", true))
			jjSetModSpeed(DesiredModSpeed = 6);
	}
}