Downloads containing EmeraldusV.j2as

Downloads
Name Author Game Mode Rating
JJ2+ Only: EmeraldusVFeatured Download Violet CLM Multiple 8.9 Download file

File preview

#include "MLLE-Include-1.4.asc"
const bool MLLESetupSuccessful = MLLE::Setup();
#pragma require "EmeraldusV-MLLE-Data-1.j2l"
#pragma require "EmeraldusV.j2l"
#pragma require "EmeraldusV.j2a"

enum AnimSets { BALL, BOSS, BRIDGE, BUZZBOMBER, CHOPPER, CRABMEAT, MOTOBUG, NEWTRON, PLATFORM, TREES, TURTLETTE, _LAST };

array<jjLAYER@> Ripples;
jjLAYER@ Clouds;

const uint8 CornerEventID = OBJECT::APPLE; //whatever

void onLevelLoad() {
	jjLAYER@ ripples = MLLE::GetLayer("Ripples");
	auto layerOrder = jjLayerOrderGet();
	const auto index = layerOrder.findByRef(ripples);
	int numberOfLayersToAdd = 24; //adjust as needed based on layer speed/level height
	while (true) {
		Ripples.insertLast(ripples);
		layerOrder.insertAt(index, ripples);
		if (--numberOfLayersToAdd > 0) {
			@ripples = jjLAYER(ripples);
			ripples.xOffset = jjRandom() & ((16*32) - 1);
			ripples.yOffset -= 16;
		} else
			break;
	}
	jjLayerOrderSet(layerOrder);
	jjUseLayer8Speeds = true;
	
	@Clouds = MLLE::GetLayer("Clouds");
	
	jjObjectPresets[CornerEventID].behavior = BEHAVIOR::INACTIVE; //zonify
	jjObjectPresets[OBJECT::ORANGE].behavior = LoopObject;
	jjObjectPresets[OBJECT::ORANGE].playerHandling = HANDLING::PARTICLE;
	AddCornerEvents();
	
	for (uint i = 0; i < AnimSets::_LAST; ++i)
		jjAnimSets[ANIM::CUSTOM[i]].load(i, "EmeraldusV.j2a");
	
	if (jjAnimSets[ANIM::BRIDGE] == 0)
		jjAnimSets[ANIM::BRIDGE].load();
	jjAnimations[jjAnimSets[ANIM::BRIDGE] + 4] = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::BRIDGE]]];
	jjObjectPresets[OBJECT::BRIDGE].behavior = MyBridge;
	
	Crabmeat(jjObjectPresets[OBJECT::BANANA]);
	Motobug(jjObjectPresets[OBJECT::CHEESE]);
	Ball(jjObjectPresets[OBJECT::BURGER]);
	GreenNewtron(jjObjectPresets[OBJECT::CHICKENLEG]);
	BlueNewtron(jjObjectPresets[OBJECT::CHIPS]);
	Chopper(jjObjectPresets[OBJECT::EGGPLANT]);
	BuzzBomber(jjObjectPresets[OBJECT::FRIES]);
	Turtlette(jjObjectPresets[OBJECT::COKE]);
	
	jjOBJ@ tree = jjObjectPresets[OBJECT::CHERRY];
	tree.behavior = Tree;
	tree.playerHandling = HANDLING::PARTICLE;
	tree.determineCurAnim(ANIM::CUSTOM[AnimSets::TREES], 0);
	MakeTreeObjects();
	
	if (jjAnimSets[ANIM::PINKPLAT] == 0)
		jjAnimSets[ANIM::PINKPLAT].load();
	for (int i = 0; i < 2; ++i)
		jjAnimations[jjAnimSets[ANIM::PINKPLAT] + i] = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::PLATFORM]] + i];
	jjObjectPresets[OBJECT::PINKPLATFORM].behavior = PlatformWrapper;
	
	jjObjectPresets[OBJECT::GRASSPLATFORM].curFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::PLATFORM]] + 3];
	jjObjectPresets[OBJECT::GRASSPLATFORM].behavior = GrassPlatform;
	jjObjectPresets[OBJECT::GRASSPLATFORM].playerHandling = HANDLING::PARTICLE;
	
	jjObjectPresets[OBJECT::WEENIE].behavior = Ropeway();
	jjObjectPresets[OBJECT::WEENIE].scriptedCollisions = true;
	jjObjectPresets[OBJECT::WEENIE].curFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::PLATFORM]] + 4];
	
	jjObjectPresets[OBJECT::GREENGEM].behavior = BEHAVIOR::INACTIVE;
	jjObjectPresets[OBJECT::BLUEGEM].behavior = BEHAVIOR::INACTIVE;
	
	jjObjectPresets[OBJECT::FROZENSPRING].freeze = 0;
	jjObjectPresets[OBJECT::FROZENSPRING].ySpeed /= 3;
	jjObjectPresets[OBJECT::FROZENSPRING].behavior = InvisibleSpring;
	
	Boss(jjObjectPresets[OBJECT::CAKE]);
}
void onDrawLayer8(jjPLAYER@ player, jjCANVAS@) {
	if (player.localPlayerID != 0 && player.isLocal)
		return;
	for (uint i = 0; i < Ripples.length; ++i)
		Ripples[i].xOffset += 0.03 * (i + 1);
	Clouds.xOffset += 0.25;
}

const float RightEdgeOfLevel = jjLayerWidth[4] * 32;
class Enemy : jjBEHAVIORINTERFACE {
	Enemy(jjOBJ@ preset, int animSpeed = 1) {
		preset.behavior = this;
		preset.playerHandling = HANDLING::ENEMY;
		preset.bulletHandling = HANDLING::HURTBYBULLET;
		preset.direction = 1;
		preset.animSpeed = animSpeed;
		preset.curFrame = jjAnimations[preset.curAnim] + preset.frameID;
		preset.isTarget = true;
		if (jjDifficulty >= 3)
			preset.energy += 1;
	}
	void onBehave(jjOBJ@ obj) {}
	void onDraw(jjOBJ@ obj) {}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) { return true; }
	
	void spawnTurtlette(jjOBJ@ obj) const {
		jjSample(obj.xPos, obj.yPos, SOUND::COMMON_GEMSMSH1, 63, 5000);
		jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos)].determineCurAnim(ANIM::AMMO, 2);
		jjAddObject(OBJECT::COKE, obj.xPos, obj.yPos, obj.creatorID, CREATOR::OBJECT);
	}
}
class WalkingEnemy : Enemy {
	int yDist;
	int halfFrameWidth;
	int walkingAnimationSpeed;
	WalkingEnemy(jjOBJ@ preset, int animSpeed) {
		super(preset, animSpeed);
		walkingAnimationSpeed = animSpeed;
		const jjANIMFRAME@ frame = jjAnimFrames[preset.curFrame];
		halfFrameWidth = frame.width / 2;
		yDist = int(abs(frame.coldSpotY - frame.hotSpotY)) + 2;
	}
	void onBehave(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::START:
				obj.putOnGround(true);
				obj.state = STATE::WALK;
				break;
			case STATE::KILL:
				spawnTurtlette(obj);
				obj.delete();
				break;
			case STATE::DEACTIVATE:
				obj.deactivate();
				break;
			case STATE::FREEZE:
				if (--obj.freeze == 0)
					obj.state = obj.oldState;
				break;
			default: {
				obj.xAcc += obj.xSpeed;
				while (obj.xAcc >= 1.f) {
					const float testHPos = obj.xPos + obj.direction;
					if (testHPos <= 0 || testHPos >= RightEdgeOfLevel)
						obj.direction = -obj.direction;
					else {
						const int topYPos = jjMaskedTopVLine(int(testHPos), int(obj.yPos), yDist);
						//jjDrawRectangle(testHPos, obj.yPos, 3,3, 16, SPRITE::NORMAL,0,2);
						bool falling = false;
						if (topYPos <= 1 || ((falling = (topYPos >= yDist)) && obj.yAcc <= 0) || jjEventAtLastMaskedPixel == AREA::STOPENEMY) {
							obj.direction = -obj.direction;
						} else { //walk forwards
							obj.xPos = testHPos;
							if (falling)
								obj.state = STATE::FALL;
							else
								obj.yPos += topYPos - (yDist - 2);
						}
					}
					obj.xAcc -= 1;
				}
				
				if (obj.state == STATE::FALL) {
					obj.yPos += obj.ySpeed += obj.yAcc;
					while (jjMaskedTopVLine(int(obj.xPos), int(obj.yPos), yDist) < yDist) {
						obj.state = STATE::WALK;
						obj.yPos -= 1;
					}
					if (obj.state == STATE::WALK) {
						obj.yPos += 1;
						obj.ySpeed = 0;
					}
				}
				
				obj.special = 0; //drawing angle
				if (obj.state == STATE::WALK) {
					const int yDist1 = jjMaskedTopVLine(int(obj.xPos - 9), int(obj.yPos), 50);
					if (yDist1 != 1 && yDist1 != 51) {
						const int yDist2 = jjMaskedTopVLine(int(obj.xPos + 9), int(obj.yPos), 50);
						if (yDist2 != 1 && yDist2 != 51) {
							obj.special = int(atan2(yDist1 - yDist2, 19) / 6.28318531 * 1024);
						}
					}
				}
				
				if (--obj.animSpeed < 0) {
					obj.animSpeed = walkingAnimationSpeed;
					obj.frameID += 1;
					obj.determineCurFrame();
				}
				
				break; }
		}
	}
	void onDraw(jjOBJ@ obj) {
		jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.special, obj.direction,1, (obj.justHit == 0) ? (obj.state != STATE::FREEZE) ? SPRITE::NORMAL : SPRITE::FROZEN : SPRITE::SINGLECOLOR, 15);
	}
}

void AddEnemyBullet(const jjOBJ@ obj, float xSpeed, float ySpeed, float yAcc, bool sample = true) {
	const jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
	jjOBJ@ bullet = jjObjects[jjAddObject(OBJECT::BULLET, obj.xPos + obj.direction * (frame.hotSpotX - frame.gunSpotX), obj.yPos + frame.hotSpotY - frame.gunSpotY, obj.objectID, CREATOR::OBJECT, EnemyBullet)];
	bullet.xSpeed = xSpeed;
	bullet.ySpeed = ySpeed;
	bullet.yAcc = yAcc;
	bullet.curFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::NEWTRON]] + 1];
	bullet.animSpeed = 1;
	bullet.playerHandling = HANDLING::ENEMYBULLET;
	bullet.light = 1;
	bullet.lightType = LIGHT::POINT2;
	if (sample)
		jjSample(bullet.xPos, bullet.yPos, SOUND::COMMON_GLASS2, 63, 30000);
	else
		bullet.counterEnd = 172;
}
void EnemyBullet(jjOBJ@ obj) {
	if (obj.state == STATE::START)
		obj.state = STATE::FLY;
	else if (obj.state == STATE::DEACTIVATE)
		obj.delete();
	else if (obj.state == STATE::EXPLODE || ++obj.counterEnd >= 210) {
		obj.curAnim = jjAnimSets[ANIM::AMMO] + 72;
		obj.behavior = BEHAVIOR::EXPLOSION;
		obj.playerHandling = HANDLING::EXPLOSION;
		jjSample(obj.xPos, obj.yPos, SOUND::COMMON_EXPSM1);
	} else {
		obj.xPos += obj.xSpeed;
		obj.yPos += obj.ySpeed += obj.yAcc;
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos + jjSin(obj.counterEnd << 4) * 2, obj.curFrame + ((jjGameTicks >> 1) & 3), int(obj.xSpeed), SPRITE::NORMAL,0, 3);
	}
}

class Motobug : WalkingEnemy {
	Motobug(jjOBJ@ preset) {
		preset.determineCurAnim(ANIM::CUSTOM[AnimSets::MOTOBUG], 0);
		preset.energy = 3;
		preset.points = 200;
		preset.xSpeed = 1.25;
		super(preset, 9);
	}
	void onBehave(jjOBJ@ obj) override {
		if (obj.state == STATE::WALK) {
			if (jjGameTicks & 31 == 0)
				jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::COMMON_LANDCAN1 + ((jjGameTicks >> 5) & 1)));
			if (obj.counter == 0) {
				const auto nearestPlayerID = obj.findNearestPlayer(160*160);
				if (nearestPlayerID >= 0) {
					obj.counter = 1;
					obj.direction = (jjPlayers[nearestPlayerID].xPos > obj.xPos) ? 1 : -1;
					jjSample(obj.xPos, obj.yPos + yDist, SOUND::COMMON_REVUP);
				}
			} else if (obj.counter < 70) {
				obj.curFrame = jjAnimations[obj.curAnim] + (++obj.counter & 1);
				jjPARTICLE@ part = jjAddParticle(PARTICLE::SPARK);
				if (part !is null) {
					part.xPos = obj.xPos;
					part.yPos = obj.yPos + yDist;
					part.xSpeed = obj.direction * (-1 - (jjRandom() & 3) / 2.f);
					part.ySpeed = (jjRandom() & 31) / 32.f;
				}
				return;
			} else if (obj.counter == 70) {
				obj.counter = 71;
				obj.xSpeed = 10;
				obj.yAcc = 0.2;
			} else {
				if (obj.xSpeed > jjObjectPresets[obj.eventID].xSpeed)
					obj.xSpeed -= 0.1;
				else {
					obj.counter = 0;
					obj.yAcc = 0;
				}
			}
		}
		WalkingEnemy::onBehave(obj);
	}
}
class Crabmeat : WalkingEnemy {
	Crabmeat(jjOBJ@ preset) {
		preset.determineCurAnim(ANIM::CUSTOM[AnimSets::CRABMEAT], 0);
		preset.energy = 1;
		preset.points = 500;
		preset.xSpeed = 1;
		super(preset, 9);
	}
	void onBehave(jjOBJ@ obj) override {
		if (obj.state != STATE::WALK || ++obj.counter < 140) {
			WalkingEnemy::onBehave(obj);
			if (obj.state == STATE::WALK && jjGameTicks & 31 == 0)
				jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::UTERUS_SCISSORS1 + (jjRandom() & 3)));
		} else if (obj.counter == 140)
			obj.curFrame = jjAnimations[obj.curAnim + 1];
		else if (obj.counter == 155)
			for (int i = -1; i < 2; i += 2)
				AddEnemyBullet(obj, i * 1.5, -6, 0.2);
		else if (obj.counter == 170) {
			obj.counter = 0;
			obj.animSpeed = 0; //start animating again
		}
	}
}

void Tree(jjOBJ@ obj) {
	if (jjLowDetail && !obj.deactivates)
		return;
	if (obj.state == STATE::START) {
		const uint xTile = uint(obj.xOrg) >> 5, yTile = uint(obj.yOrg) >> 5;
		obj.frameID = jjParameterGet(xTile,yTile, 0, 3);
		obj.curFrame = jjAnimations[obj.curAnim] + obj.frameID;
		obj.var[0] = int(jjParameterGet(xTile,yTile, 3, -5) * -8.53333333); //angle
		obj.var[1] = jjParameterGet(xTile,yTile, 8, -3) + 5; //layer... shouldn't be -4 in this level, but I guess I don't want to show a warning message?
		obj.deactivates = obj.var[1] == 5 || obj.var[1] == 6;
		obj.state = STATE::IDLE;
	} else if (obj.state == STATE::DEACTIVATE) {
		obj.deactivate();
	} else {
		const auto spriteMode = obj.var[1] <= 6 ? SPRITE::NORMAL : SPRITE::PALSHIFT;
		if (obj.frameID == 5) { //bendy tree
			if (!jjLowDetail) {
				uint trunkPieceCount = 0;
				float xPos = obj.xOrg, yPos = obj.yOrg;
				uint curFrame = obj.curFrame;
				float angle = obj.var[0];
				const float angleChange = jjSin(jjGameTicks << 1) * 3;
				while (true) {
					if (trunkPieceCount == 15)
						curFrame += 1;
					jjDrawRotatedSpriteFromCurFrame(xPos,yPos, curFrame, int(angle), 1,1, spriteMode,16, obj.var[1],obj.var[1]-1);
					if (trunkPieceCount == 15)
						break;
					++trunkPieceCount;
					angle += angleChange;
					xPos -= jjSin(int(angle)) * 5;
					yPos -= jjCos(int(angle)) * 5;
				}
			}
		} else { //single sprite
			if (obj.frameID >= 3 && jjGameTicks % 35 == 0) //sunflower
				obj.curFrame = jjAnimations[obj.curAnim] + (obj.frameID ^= 7); //animate... ^=7 switches between 3 and 4.
			jjDrawRotatedSpriteFromCurFrame(obj.xOrg, obj.yOrg, obj.curFrame, obj.var[0], 1,1, spriteMode,16, obj.var[1],obj.var[1]-1);
		}
	}
}
void MakeTreeObjects() {
	for (int x = jjLayerWidth[4]; --x >= 0;)
		for (int y = jjLayerWidth[4]; --y >= 0;)
			if (jjEventGet(x,y) == OBJECT::CHERRY) {
				jjParameterSet(x,y, -1,1, 1);
				jjAddObject(OBJECT::CHERRY, x*32 + 15, y*32 + 15, 0, CREATOR::LEVEL);
			}
}
void onLevelReload() {
	MakeTreeObjects();
}

enum BridgeVariables { PhysicalWidth, MaximumSagDistance, VisualWidth, FirstObjectIDOfPlatformForSplitscreenPlayers, Angle = FirstObjectIDOfPlatformForSplitscreenPlayers + 3 };
void MyBridge(jjOBJ@ obj) {
//first, check collision with bridge

	if (obj.state==STATE::START) {
		obj.state=STATE::STILL;

		const uint xTile = uint(obj.xOrg) >> 5, yTile = uint(obj.yOrg) >> 5;
		obj.var[BridgeVariables::PhysicalWidth] = 32 * jjParameterGet(xTile,yTile, 0,4);
		obj.curAnim = jjAnimSets[ANIM::BRIDGE].firstAnim + (jjParameterGet(xTile,yTile, 4,3) % 7); //"Type" parameter... % 7 because there are only seven bridge types for some reason.

		int toughness = jjParameterGet(xTile,yTile, 7,4);
		if (toughness == 0) toughness = 4; //default toughness of 4, to avoid dividing by zero
		obj.var[BridgeVariables::MaximumSagDistance] = obj.var[BridgeVariables::PhysicalWidth] / toughness;
		
		int heightInTiles = jjParameterGet(xTile,yTile, 11,-5);
		obj.var[BridgeVariables::Angle] = int(atan2(heightInTiles, obj.var[BridgeVariables::PhysicalWidth] / 32) * 162.974662f);
		obj.xAcc = jjCos(obj.var[BridgeVariables::Angle]);
		obj.yAcc = jjSin(obj.var[BridgeVariables::Angle]);
		
		{ //determine how wide the bridge is, in drawn pixels (will always be >= how wide it is in mask pixels)
			int frameID = 0;
			int bridge_len = 0;
			const int numberOfFramesUsedByAnimation = jjAnimations[obj.curAnim].frameCount;
			const uint firstBridgeFrameID = jjAnimations[obj.curAnim].firstFrame;
			while (true) {
				if ((bridge_len += jjAnimFrames[firstBridgeFrameID + frameID].width) >= obj.var[BridgeVariables::PhysicalWidth])
					break;
					
				if (++frameID >= numberOfFramesUsedByAnimation)
					frameID = 0;
			}
			obj.var[BridgeVariables::VisualWidth] = bridge_len;
		}

		obj.xOrg -= 16; //start at left edge of tile, not center
		obj.yOrg -= 6; //worth noting that bridges are never deactivated, so we don't need to worry about where this gets moved to at all
		
		for (int i = 1; i < jjLocalPlayerCount; ++i) { //this portion has no native JJ2 counterpart, because the API for platforms is still pretty limited
			const int platformObjectID = jjAddObject(OBJECT::BRIDGE, 0,0, obj.objectID,CREATOR::OBJECT, BEHAVIOR::BEES);
			jjOBJ@ platform = jjObjects[platformObjectID];
			platform.deactivates = false;
			platform.curFrame = jjAnimations[obj.curAnim].firstFrame;
			obj.var[BridgeVariables::FirstObjectIDOfPlatformForSplitscreenPlayers - 1 + i] = platformObjectID;
		}
	}
	
	obj.clearPlatform();
	for (int i = 1; i < jjLocalPlayerCount; ++i)
		jjObjects[obj.var[BridgeVariables::FirstObjectIDOfPlatformForSplitscreenPlayers - 1 + i]].clearPlatform();

	array<int> pressXPosition;
	array<jjPLAYER@> pressPlayer;
	for (int playerID = 0; playerID < 32; ++playerID) {
		jjPLAYER@ play = jjPlayers[playerID];
		if (play.isActive && jjObjects[play.platform].eventID != OBJECT::BURGER) { //all active players are valid, even if isLocal is false
			const int tx = int(play.xPos-obj.xOrg);
			const int ty = int(play.yPos-obj.yOrg - obj.yAcc / obj.xAcc * tx);

			if ((tx >= 0) && (tx <= obj.var[BridgeVariables::PhysicalWidth]) && //player is within bridge area (horizontal)
				(ty > -32) && (ty < obj.var[BridgeVariables::MaximumSagDistance]) && //(and vertical) //-32 was -24
				(play.ySpeed > -1.f)) //not jumping, using a spring, etc.
			{
				pressXPosition.insertLast(tx);
				pressPlayer.insertLast(play);
			}
		}
	}
	
	float max, amp, leftamp, rightamp;
	int	leftmostPressedX, rightmostPressedX;

	if (pressPlayer.length != 0) {
		if (pressPlayer.length > 1) {
			leftmostPressedX=12312312;
			rightmostPressedX=0;
			uint t = 0;
			do {
				const int pressedX = pressXPosition[t];
				if (pressedX < leftmostPressedX) 
					leftmostPressedX = pressedX;
				if (pressedX > rightmostPressedX)
					rightmostPressedX = pressedX;
			} while (++t < pressPlayer.length);

			leftamp =  obj.var[BridgeVariables::MaximumSagDistance]*jjSin((512* leftmostPressedX)/obj.var[BridgeVariables::PhysicalWidth]);
			rightamp = obj.var[BridgeVariables::MaximumSagDistance]*jjSin((512*rightmostPressedX)/obj.var[BridgeVariables::PhysicalWidth]);
		}
		
		uint t = 0;
		uint numberOfLocalPlayersNeedingPlatforms = 0;
		do {
			const auto pressedPosition = pressXPosition[t];
			if (pressPlayer.length == 1)
				max = obj.var[BridgeVariables::MaximumSagDistance] * jjSin((512 * pressedPosition) / obj.var[BridgeVariables::PhysicalWidth]); //same formula as side amps above, but for single player bridges
			else if ((pressedPosition>leftmostPressedX) && (pressedPosition<rightmostPressedX))
				max = leftamp+(rightamp-leftamp)*(pressedPosition-leftmostPressedX)/(rightmostPressedX-leftmostPressedX);
			else
				max = obj.var[BridgeVariables::MaximumSagDistance]*jjSin((512 * pressedPosition)/obj.var[BridgeVariables::PhysicalWidth]);

			jjPLAYER@ play = pressPlayer[t];
			play.yPos = obj.yOrg + obj.yAcc / obj.xAcc * pressedPosition + max - 24;
			if (play.isLocal) {
				jjOBJ@ platform = (numberOfLocalPlayersNeedingPlatforms == 0) ? obj : jjObjects[obj.var[BridgeVariables::FirstObjectIDOfPlatformForSplitscreenPlayers - 1 + numberOfLocalPlayersNeedingPlatforms]];
				platform.bePlatform(
					platform.xPos = play.xPos,
					platform.yPos = play.yPos + 24
				);
				//platform.draw();
				if (play.buttstomp < 120)
					play.buttstomp = 120;
				numberOfLocalPlayersNeedingPlatforms += 1;
			}
		} while (++t < pressPlayer.length);
	}

	//draw
	float bridge_len_x = 0, bridge_len_y = 0;
	int frameID = 0;
	const int numberOfFramesUsedByAnimation = jjAnimations[obj.curAnim].frameCount;
	while (true) {
		obj.curFrame = jjAnimations[obj.curAnim].firstFrame + frameID;
		const jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
		
		float plankOffset = 0; //"straight bridge, or terugveren"
		if (pressPlayer.length == 1) {
			const auto pressedPosition = pressXPosition[0];
			plankOffset = ((bridge_len_x<pressedPosition) ?
				(max*jjSin(int(256*bridge_len_x)/pressedPosition)) : //left
				(max*jjCos(int(256*(bridge_len_x-pressedPosition))/(obj.var[BridgeVariables::VisualWidth]-pressedPosition) ))
			);
		} else if (pressPlayer.length > 1) {
			if (bridge_len_x < leftmostPressedX)
				plankOffset = (leftamp*jjSin(int(256*bridge_len_x)/leftmostPressedX));
			else if (bridge_len_x > rightmostPressedX)
				plankOffset = (rightamp*jjCos(int(256*(bridge_len_x-rightmostPressedX))/(obj.var[BridgeVariables::VisualWidth]-rightmostPressedX) ));
			else
				plankOffset = leftamp+(rightamp-leftamp)*(bridge_len_x-leftmostPressedX)/(rightmostPressedX-leftmostPressedX);
		}
		jjDrawRotatedSpriteFromCurFrame(
			obj.xOrg + bridge_len_x - frame.hotSpotX,
			obj.yOrg + bridge_len_y + plankOffset,
			obj.curFrame,
			-obj.var[BridgeVariables::Angle]
		);

		if (int(bridge_len_x += obj.xAcc * frame.width) >= obj.var[BridgeVariables::PhysicalWidth])
			break;
		bridge_len_y += obj.yAcc * frame.width;
			
		if (++frameID >= numberOfFramesUsedByAnimation)
			frameID = 0;
	}
}

/*bool onDrawScore(jjPLAYER@, jjCANVAS@ canvas) {
	for (int x = jjResolutionWidth; --x >= 0;)
		for (int y = jjResolutionHeight; --y >= 0;)
			canvas.drawPixel(x,y, x^y);
	return false;
}*/

void PlatformWrapper(jjOBJ@ obj) {
	obj.behave(BEHAVIOR::PLATFORM, true);
	jjDrawSpriteFromCurFrame(obj.xOrg, obj.yOrg, obj.curFrame+2);
}
void GrassPlatform(jjOBJ@ obj) {
	if (obj.state == STATE::DEACTIVATE)
		obj.deactivate();
	else if (obj.state == STATE::START) {
		obj.state = STATE::FLY;
		obj.var[0] = jjParameterGet(uint(obj.xOrg) >> 5,uint(obj.yOrg) >> 5, 2,3);
	} else {
		const auto lastY = obj.yPos;
		obj.yPos = obj.yOrg + jjSin(obj.age += 4) * 6;
		const int stage = obj.var[0] == 0 ? 256 : ((obj.var[0] * 256 + jjGameTicks * 8) & 2047);
		const uint8 opacity = stage < 256 ? stage : stage > 1280 ? 0 : stage > 1024 ? (1280 - stage) : 255;
		bool stoodOn = false;
		if (opacity < 100)
			obj.clearPlatform();
		else {
			obj.bePlatform(obj.xPos, lastY);
			for (int i = 0; i < jjLocalPlayerCount; ++i)
				if (jjLocalPlayers[i].platform == obj.objectID) {
					stoodOn = true;
					break;
				}
		}
		if (stoodOn) {
			if (obj.counterEnd < 10) obj.counterEnd += 2;
		} else {
			if (obj.counterEnd != 0) obj.counterEnd -= 1;
		}
		obj.yPos += obj.counterEnd;
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame,1, SPRITE::BLEND_NORMAL, opacity);
	}
}

class Ball : WalkingEnemy {
	float lastBallX, lastBallY; //make these globalish so they persist between onBehave and onDraw, meaning the ball can be drawn exactly underneath the player (while falling) instead of one frame ahead.
	Ball(jjOBJ@ preset) {
		preset.determineCurAnim(ANIM::CUSTOM[AnimSets::BALL], 0);
		preset.energy = 5;
		preset.points = 0;
		preset.xSpeed = 1;
		super(preset, 12);
		preset.playerHandling = HANDLING::SPECIAL;
		preset.scriptedCollisions = true;
		preset.bulletHandling = HANDLING::DETECTBULLET;
		preset.yAcc = 0.1;
	}
	void onBehave(jjOBJ@ obj) override {
		if (obj.state == STATE::KILL) {
			obj.clearPlatform();
			obj.delete(); //no turtle
		} else {
			lastBallX = obj.xPos;
			lastBallY = obj.yPos;
			WalkingEnemy::onBehave(obj);
			obj.bePlatform(lastBallX, lastBallY);
		}
	}
	void onDraw( jjOBJ@ obj) override {
		jjDrawSpriteFromCurFrame(lastBallX,lastBallY, obj.curFrame, 1, (obj.justHit == 0) ? (obj.state != STATE::FREEZE) ? SPRITE::NORMAL : SPRITE::FROZEN : SPRITE::SINGLECOLOR, 15); //don't rotate, don't mirror
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) override {
		obj.scriptedCollisions = false;
		if (bullet !is null) {
			obj.bulletHandling = HANDLING::HURTBYBULLET;
			bullet.objectHit(obj, HANDLING::ENEMY);
			obj.bulletHandling = HANDLING::DETECTBULLET;
		} else {
			if (play.yPos > obj.yPos && ((play.xPos > obj.xPos) == (obj.direction == 1)))
				play.objectHit(obj, force, HANDLING::ENEMY);
		}
		obj.scriptedCollisions = true;
		return true;
	}
}


class Newtron : Enemy {
	Newtron(jjOBJ@ preset) {
		preset.determineCurAnim(ANIM::CUSTOM[AnimSets::NEWTRON], 0);
		preset.energy = 1;
		preset.points = 200;
		super(preset);
		preset.playerHandling = HANDLING::PARTICLE;
	}
	void onBehave(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::START:
				obj.state = STATE::WAIT;
				{
					const uint xTile = uint(obj.xPos) >> 5, yTile = uint(obj.yPos) >> 5;
					obj.var[0] = jjTileGet(4, xTile,yTile);
					if (obj.var[0] == 0) obj.var[0] = jjTileGet(5, xTile,yTile);
				}
				break;
			case STATE::KILL:
				spawnTurtlette(obj);
				for (int i = 0; i < 7; ++i)
					jjObjects[jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos)].determineCurAnim(ANIM::CUSTOM[AnimSets::NEWTRON], 3);
				obj.delete();
				break;
			case STATE::DEACTIVATE:
				obj.deactivate();
				break;
			case STATE::FREEZE:
				if (--obj.freeze == 0)
					obj.state = obj.oldState;
				break;
			case STATE::WAIT: {
				if (findNearbyPlayer(obj)) {
					if (obj.counterEnd == 0) {
						jjSample(obj.xPos, obj.yPos, SOUND::COMMON_COLLAPS, 63, 10000);
						if (obj.var[0] != 0)
							jjAddParticleTileExplosion(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, obj.var[0], false);
					}
					if ((obj.counterEnd += 6) >= 200) {
						obj.playerHandling = HANDLING::ENEMY;
						obj.counterEnd = 252;
						obj.state = STATE::ACTION;
					}
				} else if (obj.counterEnd > 0)
					obj.counterEnd -= 6;
			}
			break;
			case STATE::ACTION:
				action(obj);
				break;
		}
	}
	bool findNearbyPlayer(jjOBJ@ obj) {
		const auto nearestPlayerID = obj.findNearestPlayer(160*160);
		if (nearestPlayerID >= 0) {
			obj.direction = (jjPlayers[nearestPlayerID].xPos > obj.xPos) ? 1 : -1;
			return true;
		}
		return false;
	}
	void action(jjOBJ@ obj) {
	}
	void onDraw(jjOBJ@ obj) {
		int curFrame = obj.curFrame;
		if (obj.frameID == 0)
			curFrame += (jjGameTicks >> 1) & 7;
		if (obj.playerHandling == HANDLING::PARTICLE)
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, curFrame, obj.direction, SPRITE::BLEND_NORMAL, obj.counterEnd);
		else
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, curFrame, obj.direction, (obj.justHit == 0) ? (obj.state != STATE::FREEZE) ? SPRITE::NORMAL : SPRITE::FROZEN : SPRITE::SINGLECOLOR, 15);
	}
}
class GreenNewtron : Newtron {
	GreenNewtron(jjOBJ@ preset) {
		super(preset);
	}
	void action(jjOBJ@ obj) override {
		if (obj.counterEnd-- == 252)
			AddEnemyBullet(obj, obj.direction * (2 + (jjRandom() & 7) / 10.0), 0,0);
		else if (obj.counterEnd == 204) {
			if (findNearbyPlayer(obj))
				obj.counterEnd = 252;
			else {
				obj.state = STATE::WAIT;
				obj.playerHandling = HANDLING::PARTICLE;
			}
		}
	}
}

class BlueNewtron : Newtron {
	BlueNewtron(jjOBJ@ preset) {
		super(preset);
		preset.curFrame += 20;
		preset.curAnim += 2;
		preset.frameID = 8;
	}
	void action(jjOBJ@ obj) override {
		if (obj.counterEnd == 252) {
			obj.frameID = 0;
			obj.curFrame -= 8;
			obj.counterEnd = 251;
		} else if (obj.counterEnd == 251) {
			if (jjMaskedPixel(int(obj.xPos), int(obj.yPos) + 8)) {
				obj.counterEnd = 0;
				jjSample(obj.xPos, obj.yPos, SOUND::COMMON_SWISH1);
			} else
				obj.yPos += 4;
		} else {
			obj.xPos += obj.direction * 3;
		}
	}
}

class Chopper : Enemy {
	Chopper(jjOBJ@ preset) {
		preset.determineCurAnim(ANIM::CUSTOM[AnimSets::CHOPPER], 0);
		preset.energy = 2;
		preset.points = 500;
		super(preset);
	}
	void onBehave(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::START:
				obj.state = STATE::BOUNCE;
				obj.var[0] = jjTileGet(4, uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5);
				break;
			case STATE::KILL:
				spawnTurtlette(obj);
				obj.delete();
				break;
			case STATE::DEACTIVATE:
				obj.deactivate();
				break;
			case STATE::FREEZE:
				if (--obj.freeze == 0)
					obj.state = obj.oldState;
				break;
			case STATE::BOUNCE:
				obj.curFrame = jjAnimations[obj.curAnim] + ((++obj.age >> 3) & 3);
				if (obj.special == 0) {
					for (int i = 0; i < jjLocalPlayerCount; ++i) {
						if (jjLocalPlayers[i].yPos < obj.yPos) {
							const auto xDelta = obj.xPos - jjLocalPlayers[i].xPos;
							if (abs(xDelta) < 128) {
								obj.direction = xDelta > 0 ? 1 : -1;
								if (jjTileGet(4, uint(obj.xPos - 32 * obj.direction) >> 5, uint(obj.yOrg) >> 5) != uint(obj.var[0]))
									obj.direction = -obj.direction;
								obj.special = 4;
								jjSample(obj.xPos, obj.yPos, SOUND::COMMON_WATER);
							}
						}
					}
				} else if ((obj.special += 4) == 512) {
					obj.special = 0;
				} else {
					obj.yPos = obj.yOrg - jjSin(obj.special) * 192;
					obj.xPos -= obj.direction / 4.f;
				}
				break;
		}
	}
	void onDraw(jjOBJ@ obj) {
		jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.special * obj.direction, obj.direction,1, (obj.justHit == 0) ? (obj.state != STATE::FREEZE) ? SPRITE::NORMAL : SPRITE::FROZEN : SPRITE::SINGLECOLOR, 15);
	}
}

class BuzzBomber : Enemy {
	BuzzBomber(jjOBJ@ preset) {
		preset.determineCurAnim(ANIM::CUSTOM[AnimSets::BUZZBOMBER], 0);
		preset.energy = 1;
		preset.points = 1000;
		preset.state = STATE::IDLE; //no particular setup needed
		super(preset);
	}
	void onBehave(jjOBJ@ obj) {
		obj.age += 4;
		switch (obj.state) {
			case STATE::KILL:
				spawnTurtlette(obj);
				obj.delete();
				break;
			case STATE::DEACTIVATE:
				obj.deactivate();
				break;
			case STATE::FREEZE:
				if (--obj.freeze == 0)
					obj.state = obj.oldState;
				break;
			case STATE::IDLE:
				obj.var[0] = obj.findNearestPlayer(200*200);
				if (obj.var[0] >= 0) {
					obj.state = STATE::FLY;
					obj.xSpeed = 0.75 * obj.direction; //some deaccel maybe
				} else {
					obj.yPos = obj.yOrg + jjSin(obj.age) * 8;
					if (jjMaskedVLine(int(obj.xPos + obj.direction * 12), int(obj.yPos) - 8, 16))
						obj.direction = -obj.direction;
					else
						obj.xPos += 0.75 * obj.direction;
					break;
				}
			case STATE::FLY: {
				const jjPLAYER@ target = jjPlayers[obj.var[0]]; //once a target has been found, never abandon it (short of deactivation), even if there are multiple local players
				obj.direction = (target.xPos > obj.xPos) ? 1 : -1;
				const int distance = 150 + int(jjSin(obj.age) * 25);
				const float targetX = target.xPos - distance * obj.direction, targetY = target.yPos - distance;
				if (obj.xPos < targetX) {
					if (obj.xSpeed < 0.8) obj.xSpeed += 0.075;
				} else {
					if (obj.xSpeed > -0.8) obj.xSpeed -= 0.075;
				}
				if (obj.yPos < targetY) {
					if (obj.ySpeed < 0.8) obj.ySpeed += 0.075;
				} else {
					if (obj.ySpeed > -0.8) obj.ySpeed -= 0.075;
				}
				obj.ySpeed += (int(jjRandom() & 63) - 31) / 310.f;
				obj.xPos += obj.xSpeed;
				obj.yPos += obj.ySpeed;
				if (++obj.counter > 70 && abs(abs(target.xPos - obj.xPos) - (target.yPos - obj.yPos)) < 40) { //a poor man's atan
					obj.curFrame = jjAnimations[obj.curAnim] + 2;
					obj.state = STATE::ATTACK;
					obj.counter = 40;
				}
				break; }
			case STATE::ATTACK:
				if (--obj.counter == 15)
					AddEnemyBullet(obj, obj.direction * 2, 2, 0);
				else if (obj.counter == 0)
					obj.state = STATE::FLY;
		}
		if (obj.state != STATE::ATTACK && obj.state != STATE::FREEZE) {
			obj.curFrame = jjAnimations[obj.curAnim] + ((obj.age >> 4) & 1);
			obj.counterEnd = jjSampleLooped(obj.xPos, obj.yPos, SOUND::DRAGFLY_BEELOOP, obj.counterEnd);
		}
	}
	void onDraw(jjOBJ@ obj) {
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, (obj.state != STATE::FREEZE) ? (obj.justHit == 0) ? SPRITE::NORMAL : SPRITE::SINGLECOLOR : SPRITE::FROZEN,15, 3);
	}
}

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;
	int LastGems = 0;
}
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, 4), null, //floor, turn left, corner
	CornerEventResult(Floor, -3, 1), null, //wallonleft, turn left, corner
	CornerEventResult(WallOnLeft, 0, -3), null, //ceiling, turn left, corner
	CornerEventResult(Ceiling, 4, 0), null, //wallonright, turn left, corner

	null, CornerEventResult(WallOnLeft, 0, 4), //floor, turn right, corner
	null, CornerEventResult(Ceiling, -3, 0), //wallonleft, turn right, corner
	null, CornerEventResult(WallOnRight, 1, -3), //ceiling, turn right, corner
	null, CornerEventResult(Floor, 4, 1) //wallonright, turn right, corner
};
void onPlayer(jjPLAYER@ play) {
	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();
			const int maxGemsLost = 25 - (jjDifficulty * 5); //25, 20, 15, 10
			if (play.gems[GEM::RED] == 0) {
				if (play.health > 0) {
					play.health = 0;
					play.lives -= 1;
				}
			} else for (int i = 0; i < maxGemsLost && i < play.gems[GEM::RED]; ++i) {
				jjOBJ@ gem = jjObjects[jjAddObject(OBJECT::FLICKERGEM, play.xPos, play.yPos, play.playerID, CREATOR::PLAYER)];
				uint rand = jjRandom();
				gem.xSpeed = (int(rand & 15) - 7);
				gem.direction = int((rand >>= 4) & 1) * 2 - 1;
				gem.ySpeed = 0.025 - int((rand >> 1) & 7);
				gem.playerHandling = HANDLING::DELAYEDPICKUP;
				gem.var[2] = 100; //time spent unpickupable
				gem.counter /= 3; //time till death
			}
			play.gems[GEM::RED] = 0;
		}
		if (playX.LastHealth <= 0) //respawning at checkpoint
			play.gems[GEM::RED] = 0;
		playX.LastHealth = play.health;
		return;
	} else if (playX.LastGems / 100 < play.gems[GEM::RED] / 100) {
		play.lives += 1;
		jjSamplePriority(SOUND::COMMON_HARP1);
	}
	playX.LastGems = play.gems[GEM::RED];
	
	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 ? 44 : 120; //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;
	} else
		playX.LastTile = -1;
	
	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;
		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;
	}
	
	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)) {
			playX.State = PlayerState::Normal;
			playX.CurrentAngle = LoopAngle::Floor;
			playX.CornerElapsed = -1;
			play.invisibility = false;
			//play.noclipMode = 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;
			play.xSpeed = play.ySpeed = 0;
			jjDrawRotatedSprite(play.xPos, play.yPos, play.setID, play.keyRun ? RABBIT::RUN3 : RABBIT::RUN1, jjGameTicks >> 2, -angle, play.direction,1, SPRITE::PLAYER, play.playerID);
		}
	}
}

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 onMain() {
	for (int xTile = jjLayerWidth[4]; --xTile >= 0;)
		for (int yTile = jjLayerHeight[4]; --yTile >= 0;)
			if (jjEventGet(xTile,yTile) == CornerEventID)
				jjDrawString(xTile*32 + 10, yTile*32 + 10, "" + jjParameterGet(xTile,yTile, 0,4));
}*/

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 262:
				case 328:
				case 354:
					yOffset = -1;
					break;
				case 280:
				case 372:
					xOffset = -1;
					turnRight = true;
					inputAngle = LoopAngle::WallOnRight;
					break;
				case 272:
					xOffset = 1;
					turnRight = true;
					inputAngle = LoopAngle::WallOnLeft;
					break;
				case 290:
					yOffset = 1;
					inputAngle = LoopAngle::Ceiling;
					break;
				case 300:
				case 337:
				case 355:
					yOffset = -1;
					turnRight = true;
					break;
				case 322:
				case 377:
					xOffset = 1;
					inputAngle = LoopAngle::WallOnLeft;
					break;
				case 310:
					xOffset = -1;
					inputAngle = LoopAngle::WallOnRight;
					break;
				case 332:
					yOffset = 1;
					turnRight = true;
					inputAngle = LoopAngle::Ceiling;
					break;
				case 276:
					isLoop = true;
					turnRight = true;
					inputAngle = LoopAngle::Ceiling;
					break;
				case 287:
					isLoop = true;
					inputAngle = LoopAngle::WallOnRight;
					break;
				case 297:
				case 351:
					isLoop = true;
					turnRight = true;
					inputAngle = LoopAngle::WallOnRight;
					break;
				case 306:
				case 323:
				case 360:
					isLoop = true;
					break;
				case 305:
				case 326:
				case 369:
					isLoop = true;
					turnRight = true;
					break;
				case 294:
				case 358:
					isLoop = true;
					inputAngle = LoopAngle::WallOnLeft;
					break;
				case 284:
					isLoop = true;
					turnRight = true;
					inputAngle = LoopAngle::WallOnLeft;
					break;
				case 275:
					isLoop = true;
					inputAngle = LoopAngle::Ceiling;
					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 == 496 || newTileID == 498) { //side of a tree
					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) {
			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;
	}
}


class Turtlette : WalkingEnemy {
	Turtlette(jjOBJ@ preset) {
		preset.determineCurAnim(ANIM::CUSTOM[AnimSets::TURTLETTE], 0);
		preset.xSpeed = 2.5;
		preset.ySpeed = -4;
		preset.yAcc = 0.3;
		preset.deactivates = false;
		super(preset, 6);
		preset.playerHandling = HANDLING::SPECIALDONE;
		preset.isTarget = false;
		preset.state = STATE::FALL;
	}
	void onBehave(jjOBJ@ obj) override {
		if (obj.state == STATE::START && obj.creatorType == CREATOR::OBJECT)
			obj.direction = jjObjects[obj.creatorID].direction;
		if (obj.state == STATE::WALK)
			obj.yAcc = 0;
		WalkingEnemy::onBehave(obj);
	}
}

bool onDrawHealth(jjPLAYER@ player, jjCANVAS@ canvas) {
	return player.gems[GEM::RED] == 0 && jjRenderFrame & 3 < 2;
}

void InvisibleSpring(jjOBJ@ obj) { obj.behave(BEHAVIOR::SPRING, false); }

uint ChainAnim, BallAnim;
const jjANIMFRAME@ BallCollisionFrame;
class BossBall {
	int xPos = 0, yPos = 0;
	float scale = 1;
	void method(const jjOBJ@ obj, bool draw) const {
		float chainX = obj.xPos, chainY = obj.yPos;
		const float deltaX = xPos - chainX, deltaY = yPos - chainY;
		const auto angle = atan2(deltaY, deltaX);
		int stepCount = int(sqrt(deltaY*deltaY + deltaX*deltaX) / 10);
		const float stepX = cos(angle) * 10, stepY = sin(angle) * 10;
		if (draw) {
			const auto ChainFrame = jjAnimations[ChainAnim].firstFrame;
			const auto BallFrame = jjAnimations[BallAnim].firstFrame;
			while (stepCount-- > 0)
				jjDrawResizedSpriteFromCurFrame(chainX += stepX, chainY += stepY, ChainFrame, scale,scale);
			jjDrawResizedSpriteFromCurFrame(xPos, yPos - 26, BallFrame + obj.frameID, scale,scale, SPRITE::NORMAL,0, scale < 1.f ? 4 : 3);
		} else {
			while (stepCount-- > 0)
				jjObjects[jjAddObject(OBJECT::SHARD, chainX += stepX, chainY += stepY)].curAnim = ChainAnim;
			jjObjects[jjAddObject(OBJECT::SHARD, xPos, yPos - 26)].curAnim = BallAnim;
		}
	}
}
array<BossBall> BossBalls;

class Boss : Enemy {
	Boss(jjOBJ@ preset) {
		preset.determineCurAnim(ANIM::CUSTOM[AnimSets::BOSS], 0);
		preset.energy = 100;
		super(preset, 1);
		preset.deactivates = false;
		ChainAnim = jjObjectPresets[OBJECT::PINKPLATFORM].curAnim + 1;
		BallAnim = jjObjectPresets[OBJECT::BURGER].curAnim;
		@BallCollisionFrame = jjAnimFrames[jjAnimations[BallAnim]];
	}
	void onBehave(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::START:
				for (int i = 0; i < jjLocalPlayerCount; ++i) {
					jjPLAYER@ localPlayer = jjLocalPlayers[i];
					if (localPlayer.bossActivated) {
						localPlayer.boss = obj.objectID;
						obj.state = STATE::BOUNCE;
						obj.age = 256;
						BossBalls.resize(0);
						BossBalls.insertLast(BossBall());
						BossBalls[0].xPos = int(obj.xPos);
						BossBalls[0].yPos = int(obj.yPos);
						break;
					}
				}
				return;
			case STATE::FREEZE:
				if (--obj.freeze == 0)
					obj.state = obj.oldState;
				break;
			case STATE::BOUNCE:
				if (obj.counter < 180) ++obj.counter;
				obj.age += 2;
				obj.xPos = obj.xOrg + jjSin(obj.age) * 140;
				obj.yPos = obj.yOrg + jjSin(obj.age << 2) * 5;
				obj.direction = ((obj.age + 256) & 1023) < 511 ? 1 : -1;
				obj.frameID = (obj.age >> 4) & 3; //ball
				{
					BossBall@ ball = BossBalls[0];
					ball.xPos = int(obj.xPos + jjSin(obj.age) * obj.counter);
					ball.yPos = int(obj.yPos + abs(jjCos(obj.age)) * obj.counter * 5 / 6);
				}
				if (obj.age & 511 == 256 && obj.energy <= 60)
					obj.state = STATE::EXTRA;
				break;
			case STATE::EXTRA:
				if (++obj.counterEnd < 80) { //shake
					obj.xPos += int(jjRandom() & 3) - 1.5;
					obj.yPos += int(jjRandom() & 3) - 1.5;
				} else if (obj.counterEnd == 80) {
					jjAddParticlePixelExplosion(obj.xPos, obj.yPos, obj.curFrame + 2, obj.direction, 2); //screen explodes
					jjSamplePriority(SOUND::COMMON_ICECRUSH);
					obj.state = STATE::ATTACK;
				}
				break;
			case STATE::ATTACK: {
				float targetX, targetY;
				const auto nearestPlayerID = obj.findNearestPlayer(400*400);
				if (nearestPlayerID >= 0) {
					const jjPLAYER@ play = jjPlayers[nearestPlayerID];
					targetX = play.xPos;
					targetY = play.yPos;
				} else {
					targetX = obj.xOrg;
					targetY = obj.yOrg;
				}
				
				const float maxXSpeed = jjDifficulty + 1; //1, 2, 3, 4
				if (obj.xPos < targetX) {
					obj.direction = 1;
					if (obj.xSpeed < maxXSpeed) obj.xSpeed += 0.075;
				} else {
					obj.direction = -1;
					if (obj.xSpeed > -maxXSpeed) obj.xSpeed -= 0.075;
				}
				if (obj.yPos < targetY) {
					if (obj.ySpeed < 0.8) obj.ySpeed += 0.075;
				} else {
					if (obj.ySpeed > -0.8) obj.ySpeed -= 0.075;
				}
				
				obj.xPos += obj.xSpeed;
				obj.yPos += obj.ySpeed;
				const float absXSpeed = abs(obj.xSpeed);
				int targetAngle = int(atan2(obj.ySpeed, absXSpeed > 1.f ? absXSpeed : 1.f) * 162.974662f);
				if (obj.xSpeed > 0) targetAngle *= -1;
				if (targetAngle > obj.special) obj.special += 2;
				else obj.special -= 2;
				
				if (jjDifficulty > 1 && obj.energy <= 30 && BossBalls.length == 1)
					BossBalls.insertLast(BossBall());
				
				obj.age += 8;
				obj.frameID = (obj.age >> 6) & 3; //ball
				for (uint i = 0; i < BossBalls.length; ++i) {
					const auto@ ball = BossBalls[i];
					const int age = obj.age + (i << 9);
					const float dist = jjSin(age) * obj.counter;
					ball.scale = 1 + jjCos(age) / 4;
					ball.xPos = int(obj.xPos + jjCos(obj.special) * dist);
					ball.yPos = int(obj.yPos - jjSin(obj.special) * dist);
				}
				break; }
			case STATE::KILL:
				obj.state = STATE::DONE;
				obj.playerHandling = HANDLING::PARTICLE;
				obj.counterEnd = 0;
			case STATE::DONE:
				if (++obj.counterEnd < 80) { //shake
					obj.xPos += int(jjRandom() & 3) - 1.5;
					obj.yPos += int(jjRandom() & 3) - 1.5;
				} else if (obj.counterEnd == 80) {
					for (uint i = 0; i < BossBalls.length; ++i)
						BossBalls[i].method(obj, false);
				} else if (obj.counterEnd == 255) {
					obj.delete();
					jjNxt();
				} else {
					obj.doesHurt = jjSampleLooped(obj.xPos, obj.yPos, SOUND::COMMON_BENZIN1, obj.doesHurt);
					if (jjGameTicks & 3 == 1) {
						const float xPos = obj.xPos + int(jjRandom() & 63) - 31;
						const float yPos = obj.yPos + int(jjRandom() & 63) - 31;
						jjObjects[jjAddObject(OBJECT::EXPLOSION, xPos, yPos)].determineCurAnim(ANIM::AMMO, 3 + (jjRandom() & 2));
						jjOBJ@ turtlette = jjObjects[jjAddObject(OBJECT::COKE, xPos, yPos, obj.creatorID, CREATOR::OBJECT)];
						if (jjRandom() & 1 == 1) turtlette.direction = -1;
					}
				}
				return;
		}
		
		for (uint i = 0; i < BossBalls.length; ++i) {
			const auto@ ball = BossBalls[i];
			const auto ballYPos = ball.yPos - 26;
			if (abs(ball.scale - 1.f) < 0.15f) {
				for (int j = 0; j < jjLocalPlayerCount; ++j) {
					jjPLAYER@ localPlayer = jjLocalPlayers[j];
					if (localPlayer.blink == 0 && BallCollisionFrame.doesCollide(ball.xPos, ballYPos, 1, jjAnimFrames[localPlayer.curFrame], int(localPlayer.xPos), int(localPlayer.yPos), localPlayer.direction))
						localPlayer.hurt();
				}
				for (uint j = jjObjectCount; --j != 0;) {
					jjOBJ@ bullet = jjObjects[j];
					if (bullet.isActive && bullet.playerHandling == HANDLING::PLAYERBULLET && bullet.state != STATE::EXPLODE && BallCollisionFrame.doesCollide(ball.xPos, ballYPos, 1, jjAnimFrames[bullet.curFrame], int(bullet.xPos), int(bullet.yPos), bullet.direction, true))
						bullet.state = STATE::EXPLODE;
				}
			}
		}
		
		if (obj.state == STATE::FREEZE)
			return;
			
		for (int i = 0; i < jjLocalPlayerCount; ++i) {
			jjPLAYER@ localPlayer = jjLocalPlayers[i];
			if (localPlayer.invincibility < -2)
				localPlayer.invincibility = -2; //antibuttstomp
			if (obj.justHit != 0)
				localPlayer.specialMove = 0;
		}
		
		if (jjDifficulty > 0 && obj.energy < 75 && jjGameTicks & 31 == 0)
			for (int i = -1; i < 2; ++i)
				AddEnemyBullet(obj, i, -4, 0.25, false);
	}
	void onDraw(jjOBJ@ obj) {
		if (obj.state == STATE::START) //not yet activated
			return;
		if (obj.state == STATE::DONE && obj.counterEnd >= 80)
			return;
		for (uint i = 0; i < BossBalls.length; ++i)
			BossBalls[i].method(obj, true);
		const SPRITE::Mode mainDrawingMode = (obj.state != STATE::FREEZE) ? (obj.justHit == 0) ? SPRITE::NORMAL : SPRITE::SINGLECOLOR : SPRITE::FROZEN; //ship
		jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.special, 1,1, mainDrawingMode,15);
		if (jjGameTicks & 3 < 2)
			jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame + 1, obj.special, 1,1, mainDrawingMode,15); //fire
		jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame + 3, obj.special, obj.direction,1, mainDrawingMode,15); //devan
		if (obj.state == STATE::BOUNCE || (obj.state == STATE::EXTRA && obj.counterEnd < 80))
			jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame + 2, obj.special, 1,1, (obj.state != STATE::FREEZE) ? (obj.justHit == 0) ? SPRITE::TRANSLUCENT : SPRITE::TRANSLUCENTCOLOR : SPRITE::FROZEN,15); //screen
	}
}


bool onCheat(string &in cheat) {
	if (cheat == "jjgems") {
		for (int i = 0; i < jjLocalPlayerCount; ++i)
			jjLocalPlayers[i].gems[GEM::RED] = jjLocalPlayers[i].gems[GEM::RED] + 10;
		jjAlert("jjgems", false, STRING::MEDIUM);
		return true;
	}
	return false;
			
}