Downloads containing Threed.asc

Downloads
Name Author Game Mode Rating
JJ2+ Only: Threed RealmsFeatured Download Violet CLM Single player 9.7 Download file

File preview

#pragma require "Threed.asc"
#pragma require "Threed.j2a"

namespace Threed {
	enum Objects {
		Clown, GarbageCan, Puppet, PutridMoldyman, RingOfFire, SmellyGhost, TrickOrTrickKid, UrbanZombie, ZombieDog, ZombiePossessor
		, LAST
	}
	array<uint> ObjectAnimSetIDs(Objects::LAST, 0);
	
	uint GetAnimSet() {
		uint newSetID;
		uint8 customID = 0;
		do {
			newSetID = ANIM::CUSTOM[customID++];
		} while (jjAnimSets[newSetID] != 0);
		return newSetID;
	}
	
	uint GetAnimSet(Threed::Objects objec, bool reusableAnimSet = true) {
		uint newSetID;
		if (!reusableAnimSet || ObjectAnimSetIDs[objec] == 0) {
			newSetID = GetAnimSet();
			if (reusableAnimSet)
				ObjectAnimSetIDs[objec] = newSetID;
			jjAnimSets[newSetID].load(objec, "Threed.j2a");
		} else {
			newSetID = ObjectAnimSetIDs[objec];
		}
		return newSetID;
	}
	void MakeEnemy(jjOBJ@ preset) {
		preset.playerHandling = HANDLING::ENEMY;
		preset.bulletHandling = HANDLING::HURTBYBULLET;
		preset.scriptedCollisions = false;
		preset.isTarget = true;
		preset.isBlastable = true;
		preset.isFreezable = true;
		preset.triggersTNT = true;
		preset.state = STATE::START;
		preset.frameID = 0;
		preset.lightType = LIGHT::NONE;
	}
	void Apply(jjOBJ@ preset, Objects objec, bool reusableAnimSet = true, bool alternateBehavior = false) {
		if (objec != Objects::GarbageCan)
			MakeEnemy(preset);
			
		preset.curAnim = jjAnimSets[GetAnimSet(objec, reusableAnimSet)].firstAnim; //on average this is correct
		preset.curFrame = jjAnimations[preset.curAnim].firstFrame;
		
		switch (objec) {
			case Objects::Clown:
				preset.behavior = Clown;
				preset.energy = 100;
				preset.points = 500;
				preset.counter = 0;
				preset.killAnim = jjAnimSets[ANIM::AMMO] + 81;
				break;
			case Objects::GarbageCan: //mimic barrel setup
				preset.behavior = GarbageCan;
				preset.playerHandling = HANDLING::SPECIAL;
				preset.triggersTNT = true;
				preset.causesRicochet = false;
				preset.bulletHandling = HANDLING::DETECTBULLET;
				preset.isFreezable = true;
				preset.isBlastable = false;
				preset.direction = 1;
				preset.points = 100;
				preset.doesHurt = preset.eventID;
				preset.eventID = OBJECT::GUNBARREL; //for proper bullet treatment, including ricocheting
				//leave .special up to the user
				if (alternateBehavior)
					preset.var[8] = 1;
				break;
			case Objects::Puppet:
				preset.xSpeed = jjDifficulty > 0 ? 1.5 : 1;
				preset.playerHandling = HANDLING::PARTICLE;
				preset.points = 0;
				preset.killAnim = 0;
				preset.var[5] = alternateBehavior ? 1 : 0;
				preset.isTarget = false;
				preset.isFreezable = false;
				preset.deactivates = false;
				preset.behavior = Puppet(preset.eventID); //do this at the end so the above assignments have time to sink in before we start adding objects
				break;
			case Objects::PutridMoldyman:
				if (!jjSampleIsLoaded(SOUND::TURTLE_TURN))
					jjAnimSets[ANIM::TURTLE].load();
				preset.behavior = PutridMoldyman;
				preset.points = 1000;
				preset.energy = 4;
				preset.playerHandling = HANDLING::PARTICLE; //hidden
				break;
			case Objects::RingOfFire:
				preset.behavior = RingOfFire();
				preset.playerHandling = HANDLING::SPECIAL;
				preset.bulletHandling = HANDLING::IGNOREBULLET;
				preset.isTarget = false;
				preset.scriptedCollisions = true;
				preset.curAnim = jjAnimations[preset.curAnim].firstFrame;
				preset.curFrame = jjAnimations[jjAnimSets[ANIM::AMMO] + 5] + 4;
				break;
			case Objects::SmellyGhost:
				if (!jjSampleIsLoaded(SOUND::TURTLE_TURN))
					jjAnimSets[ANIM::RAPIER].load();
				preset.behavior = SmellyGhost;
				preset.energy = 2;
				preset.points = 500;
				preset.counter = 0;
				preset.special = 0;
				preset.counterEnd = 0;
				preset.lightType = LIGHT::PLAYER;
				preset.light = 9;
				break;
			case Objects::TrickOrTrickKid:
				preset.behavior = TrickOrTrickKid;
				preset.energy = 2;
				preset.points = 1000;
				preset.counter = 0;
				preset.killAnim = 0;
				break;
			case Objects::UrbanZombie:
				preset.behavior = UrbanZombie;
				preset.energy = 1;
				preset.points = 200;
				preset.xSpeed = 1;
				preset.killAnim = jjAnimSets[ANIM::AMMO] + 6;
				preset.isFreezable = false;
				break;
			case Objects::ZombieDog:
				if (jjAnimSets[ANIM::DOG] == 0)
					jjAnimSets[ANIM::DOG].load(); //for sound effects
				preset.behavior = ZombieDog;
				preset.energy = 3;
				preset.points = 200;
				preset.doesHurt = preset.eventID;
				preset.playerHandling = HANDLING::SPECIAL;
				preset.eventID = OBJECT::DOGGYDOGG;
				preset.special = preset.curAnim; //refer back to it later
				preset.curAnim += 1; //walk, not attack
				preset.curFrame = jjAnimations[preset.curAnim].firstFrame;
				preset.killAnim = jjAnimSets[ANIM::AMMO] + 6;
				break;
			case Objects::ZombiePossessor:
				if (!jjSampleIsLoaded(SOUND::RAPIER_GOSTOOOH) || !jjSampleIsLoaded(SOUND::RAPIER_GOSTDIE))
					jjAnimSets[ANIM::RAPIER].load();
				preset.behavior = ZombiePossessor;
				preset.energy = 1;
				preset.points = 200;
				preset.direction = 1;
				preset.state = STATE::IDLE;
				break;
		}
		if (preset.energy > 0 && jjDifficulty >= 3) //turbo
			preset.energy += 1;
	}
	void Apply(uint8 eventID, Objects objec, bool reusableAnimSet = true, bool alternateBehavior = false) {
		if (eventID != jjObjectPresets[eventID].eventID)
			jjDebug("Warning: jjObjectPresets[" + eventID + "].eventID has been modified.");
		Apply(jjObjectPresets[eventID], objec, reusableAnimSet, alternateBehavior);
	}


	const int ClownFrequency = 6000;
	void Clown(jjOBJ@ obj) {
		++obj.counter;
		if (obj.energy < 100 && obj.bulletHandling == HANDLING::HURTBYBULLET) { //hurt!
			jjOBJ@ ball = jjObjects[jjAddObject(obj.eventID, obj.xPos, obj.yPos - 4, obj.objectID, CREATOR::OBJECT, ClownBall)];
			ball.xSpeed = obj.xSpeed * 3 * obj.direction;
			if (ball.xSpeed == 0)
				ball.behavior = BEHAVIOR::BOUNCEONCE;
			ball.curAnim = obj.curAnim;
			ball.determineCurFrame();
			ball.ySpeed = -3;
			ball.energy = 1;
			ball.points = 100;
			
			obj.bulletHandling = HANDLING::IGNOREBULLET;
			obj.counter = 0;
			if (obj.state == STATE::FREEZE)
				obj.unfreeze(0);
			obj.state = STATE::FALL;
			obj.xSpeed = obj.direction * -3;
			obj.xAcc = obj.direction / 24.f;
			obj.ySpeed = -2;
			obj.yPos -= 30;
			obj.curFrame = jjAnimations[obj.curAnim += 3];
			obj.creatorType = CREATOR::OBJECT; //don't come back to life anymore if deactivated
		} else switch (obj.state) {
			case STATE::START:
				obj.putOnGround(false);
				obj.var[0] = jjMaskedTopVLine(int(obj.xPos), int(obj.yPos) - 40, 60);
				obj.state = STATE::ROTATE;
				obj.direction = 1;
				break;
			case STATE::DEACTIVATE:
				obj.deactivate();
				break;
			case STATE::FREEZE:
				--obj.counter;
				if (--obj.freeze == 0)
					obj.state = obj.oldState;
				if (obj.oldState != STATE::WAIT && obj.oldState != STATE::FALL)
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, jjAnimations[obj.curAnim], obj.direction);
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::FROZEN);
				break;
			case STATE::ROTATE: {
				const auto sine = jjSin(obj.counter << 1);
				obj.xSpeed = sine * (3 + jjDifficulty / 2.f);
				const float targetX = obj.xPos + obj.xSpeed * obj.direction;
				if (jjMaskedTopVLine(int(targetX), int(obj.yPos) - 40, 60) == obj.var[0] && jjEventAtLastMaskedPixel != AREA::STOPENEMY)
					obj.xPos = targetX;
				else {
					obj.direction = -obj.direction;
					jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::SPAZSOUNDS_HAHAHA + (jjRandom() & 1)), 0, ClownFrequency);
				}
				obj.xAcc += sine;
				obj.frameID = int(obj.xAcc) / 4;
				obj.curAnim += 1;
				obj.determineCurFrame(true);
				obj.curAnim -= 1;
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.determineCurFrame(false), obj.direction);
				const int angle = int(sine * obj.direction * 96);
				const float asin = jjSin(angle), acos = jjCos(angle);
				const int xdiff = 6 * obj.direction;
				jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, angle, obj.direction,1, obj.justHit == 0 ? SPRITE::NORMAL : SPRITE::SINGLECOLOR, 15);
				jjDrawSpriteFromCurFrame(obj.xPos - (asin * 33 + acos * xdiff), obj.yPos - (acos * 33 - asin * xdiff), jjAnimations[obj.curAnim + 5] + ((jjGameTicks >> 2) % 12), obj.direction);
				if (obj.counter == 256) {
					obj.counter = 0;
					obj.state = STATE::ATTACK;
				}
				break; }
			case STATE::ATTACK:
				obj.curFrame = jjAnimations[obj.curAnim + 2] + obj.counter / 10;
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, jjAnimations[obj.curAnim], obj.direction);
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction);
				if (obj.counter == 20) {
					jjSample(obj.xPos, obj.yPos, SOUND::SPAZSOUNDS_HIHI, 0, ClownFrequency * 3 / 5);
					jjOBJ@ bomb = jjObjects[obj.fireBullet(OBJECT::BOMB)];
					bomb.curAnim = obj.curAnim + 6;
					bomb.xSpeed = obj.direction * 2.5;
					bomb.ySpeed = -3;
					bomb.state = STATE::FLY;
					if (jjDifficulty > 2) {
						bomb.playerHandling = HANDLING::ENEMYBULLET;
						bomb.animSpeed = 1;
					}
				} else if (obj.counter == 49) {
					obj.counter = 0;
					obj.state = STATE::ROTATE;
				}
				break;
			case STATE::FALL: {
				if ((obj.xSpeed > 0) == (obj.direction == -1)) {
					const float targetX = obj.xPos + (obj.xSpeed += obj.xAcc);
					if (!jjMaskedVLine(int(targetX), int(obj.yPos) - 12, 24))
						obj.xPos = targetX;
					else
						obj.xSpeed = obj.xAcc = 0;
				}
				const float targetY = obj.yPos + (obj.ySpeed += 0.09);
				if (!jjMaskedHLine(int(obj.xPos) - 24, 24, int(targetY) + 18)) {
					obj.yPos = targetY;
					if (obj.counter == 20)
						++obj.curFrame;
				} else {
					obj.curFrame = jjAnimations[obj.curAnim] + 2;
					obj.state = STATE::WAIT;
					obj.counter = 0;
				}
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, obj.justHit == 0 ? SPRITE::TRANSLUCENT : SPRITE::TRANSLUCENTCOLOR, 15);
				break; }
			case STATE::WAIT:
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::TRANSLUCENT);
				if (obj.counter == 35)
					obj.curFrame += 1;
				else if (obj.counter == 50) {
					jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::SPAZSOUNDS_HAHAHA + (jjRandom() & 1)), 0, ClownFrequency);
					obj.bulletHandling = HANDLING::HURTBYBULLET;
					obj.curAnim += 1;
					obj.frameID = 0;
					obj.determineCurFrame();
					obj.state = STATE::WALK;
					obj.behavior = BEHAVIOR::WALKINGENEMY;
					obj.xSpeed = 4 * obj.direction;
					obj.xAcc = 0;
					obj.energy = (jjDifficulty > 2) ? 4 : 3;
				}
				break;
		}
	}
	void ClownBall(jjOBJ@ obj) { //minimally edited from BEHAVIOR::TURTLESHELL to avoid killing enemies
		if (obj.state==STATE::START)
			obj.state=STATE::FLY;
		else if (obj.state==STATE::KILL || obj.state==STATE::DEACTIVATE)
			obj.delete();
		else if (obj.state == STATE::FREEZE) {
			if (--obj.freeze == 0)
				obj.state = STATE::FLY;
		} else {
			if (abs(obj.xSpeed) > 2) {
				if (obj.yPos>jjWaterLevel)
				{
					obj.xSpeed=(obj.xSpeed*7)/8;
					obj.ySpeed=(obj.ySpeed*7)/8;
				}
				else
					obj.xSpeed=(obj.xSpeed*63)/64;
			}

			obj.xPos+=obj.xSpeed;
			obj.yPos+=obj.ySpeed;

			const bool nogoup=jjMaskedPixel(int(obj.xPos),int(obj.yPos)-4);
			const bool nogodown=jjMaskedPixel(int(obj.xPos),int(obj.yPos)+10);
			const bool nogoleft=jjMaskedPixel(int(obj.xPos)-8,int(obj.yPos));
			const bool nogoright=jjMaskedPixel(int(obj.xPos)+8,int(obj.yPos));
			int calc, calc2;

			if ((obj.xSpeed>0) && nogoright)
			{
				calc=1+int(obj.xSpeed);

				calc2=jjMaskedTopVLine(int(obj.xPos+8),int(obj.yPos)+10-calc,calc);

				if ((calc2<=1) || (calc2>=calc))
				{
					obj.xPos-=obj.xSpeed;
					obj.xSpeed=-obj.xSpeed;
				} else
				{
					obj.yPos-=calc;
					obj.xSpeed=(obj.xSpeed*15)/16;
				};
			} else
			if ((obj.xSpeed<0) && nogoleft)
			{
				calc=1-int(obj.xSpeed);

				calc2=jjMaskedTopVLine(int(obj.xPos-8),int(obj.yPos)+10-calc,calc);

				if ((calc2<=1) || (calc2>=calc))
				{
					obj.xPos-=obj.xSpeed;
					obj.xSpeed=-obj.xSpeed;
				} else
				{
					obj.yPos-=calc;
					obj.xSpeed=(obj.xSpeed*15)/16;
				};
			};

			if (nogoup && (obj.ySpeed<0))
			{
				obj.yPos-=obj.ySpeed; //reset to last location!
				obj.ySpeed=-obj.ySpeed/2;
			} else
			if (nogodown)
			{
				calc=jjMaskedTopVLine(int(obj.xPos),int(obj.yPos),10);

				if (calc != 0)
					obj.yPos-=(10-calc);
				else
					obj.yPos-=obj.ySpeed;

				if (obj.ySpeed>1)
					jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::COMMON_IMPACT1 + (jjRandom() & 7)));

				obj.ySpeed=-(obj.ySpeed/2);
			} else
			{
				if (obj.yPos>jjWaterLevel)
				{
					obj.ySpeed+=0.125/4;
					if (obj.ySpeed>4) obj.ySpeed=4;
					else
					if (obj.ySpeed<-4) obj.ySpeed=-4;
				}
				else
				{
					obj.ySpeed+=0.125;
					if (obj.ySpeed>8) obj.ySpeed=8;
				};
			};
		}

		obj.direction = (obj.xSpeed >= 0) ? 1 : -1;
		if (jjGameTicks & 3 == 0) {
			++obj.frameID;
			obj.determineCurFrame();
		}
		obj.draw();
	}

	jjVOIDFUNCOBJ@ GarbageCanCallback = null;
	void GarbageCan(jjOBJ@ obj) {
		if (obj.state == STATE::ACTION) {
			obj.eventID = obj.doesHurt;
			const int originalCurAnim = obj.curAnim;
			obj.curAnim = jjAnimSets[ANIM::PICKUPS] + 3; //pretend to be a barrel
			obj.behave(BEHAVIOR::CRATE, true);
			bool foundLid = obj.var[7] == 1;
			for (int i = jjObjectCount; --i >= 0;) {
				jjOBJ@ shard = jjObjects[i];
				if (shard.eventID == OBJECT::SHARD && shard.isActive && shard.xOrg == obj.xPos && shard.yOrg == obj.yPos) {
					if (!foundLid) { //only one lid shard
						shard.curAnim = originalCurAnim + 1;
						foundLid = true;
					} else //misc
						shard.curAnim = originalCurAnim + 2 + (jjRandom() & 1);
				}
			}
			if (obj.var[8] != 0 && GarbageCanCallback !is null)
				GarbageCanCallback(obj);
		} else {
			if (obj.state == STATE::DEACTIVATE)
				obj.eventID = obj.doesHurt;
			obj.behave(BEHAVIOR::CRATE, true);
		}
	}

	void SmellyGhost(jjOBJ@ obj) {
		if (obj.state == STATE::START) {
			if (obj.creatorType == CREATOR::LEVEL) {
				obj.state = STATE::IDLE;
			} else { //from a garbage can?
				obj.state = STATE::ROTATE;
				obj.playerHandling = HANDLING::PARTICLE;
			}
			obj.direction = int(jjRandom() & 1) * 2 - 1;
		}
		
		switch (obj.state) {
			case STATE::IDLE:
				obj.counter += 4;
				obj.xPos += jjSin(obj.counter) / 12 * obj.direction;
				obj.yPos += jjCos(obj.counter) / 8;
				if (obj.counter > 100) {
					const int playerID = obj.findNearestPlayer(160 * 160);
					if (playerID >= 0) {
						obj.var[0] = playerID;
						obj.counter = 0;
						obj.state = STATE::ATTACK;
					jjSample(obj.xPos, obj.yPos, SOUND::RAPIER_GOSTOOOH, 63, 16000);
					}
				}
				break;
			case STATE::ATTACK: {
				const jjPLAYER@ target = jjPlayers[obj.var[0]];
				if (++obj.counter == 20) {
					obj.counter = 0;
					if (++obj.special == 3) {
						obj.state = STATE::FIRE;
						obj.var[1] = int(atan2(
							target.yPos - obj.yPos,
							target.xPos - obj.xPos
						) * 1024 / 6.283185307179586);
					}
				}
				obj.direction = target.xPos > obj.xPos ? 1 : -1;
				break; }
			case STATE::FIRE:
				if (jjGameTicks & 3 == 1)
					jjAddObject(OBJECT::BULLET, obj.xPos, obj.yPos - 8, obj.objectID, CREATOR::OBJECT, SmellyGhostBee);
				if (++obj.counter >= jjDifficulty * 25 + 45) {
					obj.state = STATE::IDLE;
					obj.counter = 0;
					obj.special = 0;
				}
				break;
			case STATE::ROTATE:
				if ((obj.counter += 4) >= 256) {
					obj.state = STATE::IDLE;
					obj.playerHandling = HANDLING::ENEMY;
				}
				obj.xPos = obj.xOrg - jjSin(obj.counter) * 60 * obj.direction;
				obj.yPos = obj.yOrg - 110 + jjCos(obj.counter) * 110;
				break;
			case STATE::KILL:
				jjSample(obj.xPos, obj.yPos, SOUND::RAPIER_GOSTDIE);
				obj.delete();
				return;
			case STATE::DEACTIVATE:
				obj.deactivate();
				return;
			case STATE::FREEZE:
				if ((obj.freeze -= 1 )<= 0) {
					obj.state = obj.oldState;
					obj.unfreeze(0);
				}
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::FROZEN);
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, jjAnimations[obj.curAnim + 1] + obj.special, obj.direction, SPRITE::FROZEN);
				return;
		}
		
		if (++obj.counterEnd == 9) {
			obj.frameID += 1;
			obj.counterEnd = 0;
			obj.determineCurFrame();
		}
		
		const SPRITE::Mode mode = obj.justHit == 0 ? SPRITE::NORMAL : SPRITE::SINGLECOLOR;
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, mode, 15);
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, jjAnimations[obj.curAnim + 1] + obj.special, obj.direction, mode, 15);
	}
	void SmellyGhostBee(jjOBJ@ obj) {
		if (obj.state == STATE::START) {
			const jjOBJ@ creator = jjObjects[obj.creatorID];
			obj.playerHandling = HANDLING::ENEMYBULLET;
			obj.curAnim = creator.curAnim + 2;
			obj.curFrame = jjAnimations[obj.curAnim];
			const int angle = creator.var[1] + int(jjRandom() & 63) - 32;
			obj.xSpeed = jjCos(angle) * 2;
			obj.ySpeed = jjSin(angle) * 2;
			obj.xAcc = obj.yAcc = 0;
			obj.direction = (obj.xSpeed >= 0) ? 1 : -1;
			//obj.behavior = SmellyGhostBee;
			obj.counter = 1;
			obj.counterEnd = 145;
			MakeEnemy(obj);
			obj.state = STATE::FLY;
			obj.energy = 1;
			obj.points = 10;
			obj.lightType = LIGHT::NONE;
		}
		else if (obj.counter >= int(obj.counterEnd) || jjMaskedPixel(int(obj.xPos + obj.xSpeed), int(obj.yPos + obj.ySpeed)))
			obj.delete(); //avoid playing explosion sound
		else
			obj.behave(BEHAVIOR::BULLET);
	}
	
	const array<int> LidPeekFrames = {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0};
	void PutridMoldyman(jjOBJ@ obj) {
		jjOBJ@ can = jjObjects[obj.special];
		switch (obj.state) {
			case STATE::START: {
				obj.state = STATE::DELAYEDSTART;
				obj.special = jjAddObject(OBJECT::GUNBARREL, obj.xOrg, obj.yOrg, obj.objectID, CREATOR::OBJECT, BEHAVIOR::INACTIVE);
				@can = jjObjects[obj.special];
				Apply(can, Objects::GarbageCan);
				can.deactivates = false;
				can.behave(BEHAVIOR::DEFAULT, false);
				obj.yPos = can.yPos;
				can.var[7] = 1; //don't spawn another lid shard later
				can.xSpeed = 0.2;
			}
			case STATE::DELAYEDSTART: {
				const int nearestPlayerID = can.findNearestPlayer(90 * 90);
				if (nearestPlayerID >= 0) {
					obj.state = STATE::IDLE;
					obj.playerHandling = HANDLING::ENEMY;
					jjSample(can.xPos, can.yPos, SOUND::TURTLE_TURN);
					jjSample(can.xPos, can.yPos, SOUND::COMMON_SPLUT);
					if (jjDifficulty > 0) {
						for (int i = 4; --i >= 0;)
							jjAddObject(OBJECT::SHARD, can.xPos, can.yPos - 26, obj.objectID, CREATOR::OBJECT, function(h){h.behavior = PutridSpore; h.behave();});
						if (jjPlayers[nearestPlayerID].invincibility < 0) //recently stomped something
							jjPlayers[nearestPlayerID].invincibility = 0; //be vulnerable to my spores
					}
					jjObjects[jjAddObject(OBJECT::SHARD, can.xPos, can.yPos, obj.objectID, CREATOR::OBJECT)].curAnim = jjObjects[obj.special].curAnim + 1; //lid
				} else {
					const int frameID = LidPeekFrames[(jjGameTicks >> 2) % LidPeekFrames.length];
					if (frameID >= 0)
						jjDrawSpriteFromCurFrame(can.xPos, can.yPos, jjAnimations[obj.curAnim + 1] + frameID, 1, SPRITE::NORMAL,0, 3);
				}
				return; }
			case STATE::KILL:
				jjSample(obj.xPos, obj.yPos, SOUND::COMMON_SPLUT);
				obj.particlePixelExplosion(104);
				if (can.behavior == GarbageCan && can.creatorID == uint(obj.objectID))
					can.state = STATE::ACTION;
				obj.delete();
				break;
			case STATE::DEACTIVATE:
				if (can.behavior == GarbageCan && can.creatorID == uint(obj.objectID))
					can.deactivate();
				obj.deactivate();
				return;
			case STATE::IDLE:
				if (can.behavior != GarbageCan || can.creatorID != uint(obj.objectID))
					obj.delete();
				else {
					if (jjGameTicks & 3 == 3) {
						jjPARTICLE@ bubble = jjAddParticle(PARTICLE::RAIN);
						if (bubble !is null) {
							bubble.xPos = obj.xPos - 16 + (jjRandom() & 31);
							bubble.yPos = obj.yPos - 3;
							bubble.ySpeed = 1.8;
						}
					}
					if (jjGameTicks & 7 == 7) {
						jjPARTICLE@ smoke = jjAddParticle(PARTICLE::SMOKE);
						if (smoke !is null) {
							smoke.xPos = obj.xPos - 16 + (jjRandom() & 31);
							smoke.yPos = obj.yPos - 5 - int(jjRandom() & 31);
						}
					}
					if (jjGameTicks & 15 == 15) {
						jjAddObject(OBJECT::EXPLOSION, obj.xPos - 16 + (jjRandom() & 31), obj.yPos - 32 + (jjRandom() & 31), obj.objectID, CREATOR::OBJECT, PutridExplosion);
					}
					
				}
				break;
			case STATE::FREEZE:
				if ((obj.freeze -= 1 )<= 0) {
					obj.state = obj.oldState;
					obj.unfreeze(0);
				} else {
					obj.xPos = can.xPos;
					obj.yPos = can.yPos;
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 0, SPRITE::FROZEN, 0, 3);
					return;
				}
				break;
		}
		
		if (obj.special != 0) { //edge cases or something idk
			obj.xPos = can.xPos;
			obj.yPos = can.yPos;
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 0, obj.justHit == 0 ? SPRITE::NORMAL : SPRITE::SINGLECOLOR, 15, 3);
		} else {
			obj.delete();
		}
	}
	void PutridExplosion(jjOBJ@ obj) {
		if (obj.state == STATE::START) {
			obj.determineCurAnim(ANIM::PICKUPS, 86);
			obj.lightType = LIGHT::NONE;
		}
		obj.behave(BEHAVIOR::EXPLOSION, false);
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::SINGLECOLOR, 68);
	}
	int SporeXSpeed = -3;
	class PutridSporeClass : jjBEHAVIORINTERFACE {
		void onBehave(jjOBJ@ obj) {
			if (obj.state == STATE::START) {
				obj.curFrame = jjAnimations[jjObjects[obj.creatorID].curAnim + 2];
				obj.playerHandling = HANDLING::PICKUP;
				obj.scriptedCollisions = true;
				obj.ySpeed = -1.4 + abs(SporeXSpeed) / 14;
				obj.xSpeed = SporeXSpeed / 7.f;
				if ((SporeXSpeed += 2) > 3)
					SporeXSpeed = -3;
				obj.state = STATE::FLY;
			} else if (obj.state == STATE::EXPLODE || obj.state == STATE::DEACTIVATE || jjMaskedPixel(int(obj.xPos), int(obj.yPos))) {
				obj.delete();
			} else {
				obj.xPos += obj.xSpeed;
				obj.yPos += obj.ySpeed += 0.01;
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame);
			}
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@, jjPLAYER@ play, int) {
			const auto realButtstomp = play.buttstomp;
			play.buttstomp = 121;
			if (!play.hurt(1))
				play.buttstomp = realButtstomp;
			obj.delete();
			return true;
		}
	}
	PutridSporeClass PutridSpore;
	
	void ZombiePossessor(jjOBJ@ obj) {
		//if (obj.state != STATE::FREEZE)
		//	jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::BLEND_SCREEN,160);
		
		obj.age += 4;
		switch (obj.state) {
			case STATE::KILL:
				if (obj.counter > 40 && jjDifficulty > 0)
					jjAddObject(obj.eventID, obj.xPos, obj.yPos, obj.objectID, CREATOR::OBJECT, function(h){h.behavior = ZombiePossessorIcyHand; h.behave();});
				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 (target.frozen == 0 && ++obj.counter > 70 && abs(abs(target.xPos - obj.xPos) - (target.yPos - obj.yPos)) < 40) { //a poor woman's atan
					obj.state = STATE::ATTACK;
					jjAddObject(obj.eventID, obj.xPos, obj.yPos, obj.objectID, CREATOR::OBJECT, function(h){h.behavior = ZombiePossessorIcyHand;});
					obj.counter = 25;
				}
				break; }
			case STATE::ATTACK:
				if (--obj.counter <= 0)
					obj.state = STATE::FLY;
				break;
		}
		
		if (obj.state != STATE::FREEZE) {
			obj.frameID = obj.age >> 4;
			obj.determineCurFrame();
			obj.var[1] = jjSampleLooped(obj.xPos, obj.yPos, SOUND::RAPIER_GOSTLOOP, obj.var[1]);
		}
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, (obj.state != STATE::FREEZE) ? (obj.justHit == 0) ? SPRITE::TRANSLUCENT : SPRITE::SINGLECOLOR : SPRITE::FROZEN,15);
	}
	class ZombiePossessorIcyHandClass : jjBEHAVIORINTERFACE {
		void onBehave(jjOBJ@ obj) {
			if (obj.state == STATE::DEACTIVATE) {
				obj.delete();
				return;
			}
			if (obj.state == STATE::IDLE) { //ersatz START
				obj.curFrame = jjAnimations[obj.curAnim + 1];
				const jjOBJ@ creator = jjObjects[obj.creatorID];
				obj.direction = creator.direction;
				obj.var[0] = creator.var[0];
				obj.xSpeed = obj.direction * 3;
				obj.ySpeed = 3;
				obj.counterEnd = 5;
				obj.state = STATE::DELAYEDSTART;
				obj.animSpeed = 1;
				obj.playerHandling = HANDLING::PARTICLE; //still forming
				obj.bulletHandling = HANDLING::DETECTBULLET;
				obj.isFreezable = false;
				obj.isTarget = false;
			}
			if (obj.state == STATE::DELAYEDSTART) {
				if ((obj.counterEnd += 10) == 255) {
					obj.state = STATE::FLY;
					obj.playerHandling = HANDLING::SPECIAL;
					obj.scriptedCollisions = true;
				} else
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::BLEND_DISSOLVE, obj.counterEnd);
			}
			if (obj.state == STATE::EXPLODE || (obj.yPos > jjPlayers[obj.var[0]].yPos && jjMaskedPixel(int(obj.xPos), int(obj.yPos)))) {
				obj.unfreeze(1);
				obj.delete();
			} else if (obj.state == STATE::FLY) { //this check may not be needed
				obj.xPos += obj.xSpeed;
				obj.yPos += obj.ySpeed;
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction);
			}
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) {
			if (bullet is null) {
				if (play.shieldType != SHIELD::FIRE) {
					if (play.blink == 0 && jjDifficulty != 1) { //not normal
						play.freeze();
						obj.state = STATE::EXPLODE;
					}
					if (jjDifficulty > 0) { //not easy
						if (play.hurt())
							obj.state = STATE::EXPLODE;
					}
				} else {
					obj.particlePixelExplosion(1);
					obj.delete();
				}
			} else {
				if (bullet.var[6] & 2 == 2) { //fire
					obj.particlePixelExplosion(1);
					obj.delete();
				} else
					bullet.state = STATE::EXPLODE;
			}
			return true;
		}
	}
	ZombiePossessorIcyHandClass ZombiePossessorIcyHand();
	
	void ZombieDog(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::WALK:
				obj.xSpeed = obj.direction / 2.f;
				obj.curAnim = obj.special + 1;
				obj.behave(BEHAVIOR::WALKINGENEMY);
				if (++obj.counter > obj.var[3]) {
					obj.counter = 0;
					obj.var[3] = 30 + (jjRandom() & 31);
					if (obj.var[3] & 1 == 1)
						jjSample(obj.xPos, obj.yPos, SOUND::DOG_SNIF1, 40,0);
				}
				return;
			case STATE::DEACTIVATE:
				obj.eventID = obj.doesHurt;
				break;
			case STATE::ACTION:
				obj.behave(BEHAVIOR::DOGGYDOGG, false);
				obj.curAnim = obj.special;
				obj.determineCurFrame();
				obj.draw();
				return;
		}
		obj.behave(BEHAVIOR::DOGGYDOGG);
	}

	void UrbanZombie(jjOBJ@ obj) {
		switch (obj.state) {
		case STATE::START:
			if (obj.creator == CREATOR::LEVEL && jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0, 1) == 1) {
				obj.state = STATE::DELAYEDSTART;
				obj.playerHandling = HANDLING::PARTICLE;
				obj.lightType = LIGHT::POINT;
				obj.putOnGround();
				obj.yPos = (int(obj.yPos) & ~31) + 42;
			} else
				obj.state = STATE::WALK;
			return;
		case STATE::DELAYEDSTART: {
			const int nearestPlayerID = obj.findNearestPlayer(110 * 110);
			if (nearestPlayerID >= 0) {
				obj.state = STATE::JUMP;
				obj.direction = jjPlayers[nearestPlayerID].xPos > obj.xPos ? 1 : -1;
				obj.yPos = obj.yOrg;
				jjSample(obj.xPos, obj.yPos, SOUND::DOG_AGRESSIV, 0, 14000);
			} else {
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame + ((jjGameTicks / 13) % 12), SPRITE::FLIPV);
				return;
			}
		}
		case STATE::JUMP:
			obj.yPos = obj.yOrg - jjSin(obj.age += 8) * 75;
			obj.frameID = obj.age / 40;
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.determineCurFrame(), obj.direction, layerZ: obj.age < 256 ? 7 : 4);
			if (obj.age == 256)
				obj.playerHandling = HANDLING::ENEMY;
			else if (obj.age == 512)
				obj.state = STATE::WALK;
			break; //fire
		case STATE::DEACTIVATE:
			obj.deactivate();
			return;
		case STATE::KILL:
			if (jjDifficulty == 1 || jjDifficulty == 2)
				for (uint objectID = jjObjectCount; --objectID != 0;) {
					const jjOBJ@ bullet = jjObjects[objectID];
					if (bullet.eventID == obj.eventID && int(bullet.creatorID) == obj.objectID)
						jjDeleteObject(objectID);
				}
		default:
			obj.behave(BEHAVIOR::WALKINGENEMY, true);
			break; //fire
		}
		if (jjDifficulty > 0 && obj.state != STATE::FREEZE && obj.state != STATE::KILL && jjGameTicks & 3 == 0) {
			jjAddObject(obj.eventID, obj.xPos, obj.yPos - 10, obj.objectID, CREATOR::OBJECT, function(h){h.behavior = UrbanZombieArcticColdBreath; h.behave();});
		}
	}
	class UrbanZombieArcticColdBreathClass : jjBEHAVIORINTERFACE {
		void onBehave(jjOBJ@ obj) {
			if (obj.state == STATE::START) {
				obj.doesHurt = 32 + (jjRandom() & 3);
				obj.isTarget = false;
				obj.playerHandling = HANDLING::SPECIAL;
				obj.bulletHandling = HANDLING::IGNOREBULLET;
				obj.scriptedCollisions = true;
				obj.curAnim = 27;
				obj.determineCurFrame();
				obj.killAnim = 0;
				const jjOBJ@ creator = jjObjects[obj.creatorID];
				if (creator.state != STATE::JUMP) {
					obj.direction = creator.direction;
					obj.xSpeed = 1.5 * obj.direction;
					obj.xAcc = float(jjRandom() & 63) / 1280 * obj.direction;
					obj.yAcc = float(jjRandom() & 63) / 1280 - 0.025;
				} else {
					obj.curAnim += 4;
					obj.xSpeed = 0;
					obj.ySpeed = -1.5;
					obj.yAcc = float(jjRandom() & 63) / -1280;
					obj.xAcc = float(jjRandom() & 63) / 1280 - 0.025;
				}
				obj.state = STATE::FLY;
			}
			if (++obj.counter >= 70)
				obj.delete();
			else {
				obj.xPos += obj.xSpeed += obj.xAcc;
				obj.yPos += obj.ySpeed += obj.yAcc;
				if (obj.counter >= 9)
					jjDrawSprite(obj.xPos, obj.yPos, ANIM::AMMO, obj.curAnim, obj.counter >> 1, obj.direction, SPRITE::TRANSLUCENT);
			}
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) {
			if (bullet is null && play.shieldType != SHIELD::FIRE)
				play.frozen = 70; //one third the time of a freeze enemies pickup
			return true;
		}
	}
	UrbanZombieArcticColdBreathClass UrbanZombieArcticColdBreath();

	void TrickOrTrickKid(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::START:
				obj.direction = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0, 1) * 2 - 1;
				if (jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 1, 1) == 1) { //maybe true if generating, depending on eventID
					obj.behavior = TrickOrTrickKidPumpkinBomb;
					obj.behave();
					return;
				}
				obj.putOnGround(true);
				obj.state = STATE::WAIT;
				obj.special = jjAddObject(obj.eventID, 0,0, obj.objectID, CREATOR::OBJECT, function(h){h.behavior = TrickOrTrickKidPumpkinBomb; h.behave();});
				break;
			case STATE::DEACTIVATE:
				jjObjects[obj.special].delete();
				obj.deactivate();
				return;
			case STATE::KILL: {
				jjOBJ@ pumpkin = jjObjects[obj.special];
				if (jjDifficulty <= 0) {
					pumpkin.particlePixelExplosion(2);
					pumpkin.delete();
				} else {
					pumpkin.state = STATE::FALL;
					pumpkin.deactivates = true;
					pumpkin.isTarget = false;
					pumpkin.bulletHandling = HANDLING::DESTROYBULLET;
				}
				obj.delete();
				return; }
			case STATE::FREEZE:
				if (--obj.freeze <= 0)
					obj.state = obj.oldState;
				break;
			case STATE::WAIT:
				obj.frameID = ++obj.counter >> 2;
				obj.determineCurFrame();
				if (obj.counter > 70) {
					const int nearest = obj.findNearestPlayer(160 * 160);
					if (nearest >= 0 && ((jjPlayers[nearest].xPos > obj.xPos) == (obj.direction == 1))) {
						obj.counter = 0;
						obj.frameID = 0;
						obj.state = STATE::ATTACK;
						obj.curAnim += 2;
					}
				}
				break;
			case STATE::ATTACK:
				obj.frameID = obj.counter++ >> 2;
				if (obj.frameID >= 15) {
					obj.state = STATE::WAIT;
					obj.counter = 0;
					obj.curAnim -= 2;
					break;
				} else if (obj.frameID >= 8) {
					obj.frameID = 15 - obj.frameID;
				} else if (obj.counter == 30) {
					for (int i = 2 + jjDifficulty; --i >= 0; ) {
						jjOBJ@ seed = jjObjects[jjAddObject(OBJECT::BULLET, obj.xPos + 20 * obj.direction, obj.yPos - 8, obj.objectID, CREATOR::OBJECT, BEHAVIOR::BULLET)];
						seed.curAnim = obj.curAnim + 1;
						seed.xSpeed = (0.5 + i * 0.7) * obj.direction;
						seed.ySpeed = -(1.2 + i * 0.4);
						seed.xAcc = 0.01 * obj.direction;
						seed.yAcc = 0.065;
						seed.animSpeed = (jjDifficulty >= 2) ? 2 : 1;
						seed.playerHandling = HANDLING::ENEMYBULLET;
						seed.killAnim = jjAnimSets[ANIM::AMMO] + 70;
						seed.counterEnd = 110;
					}
				}
				obj.determineCurFrame();
				break;
		}
		obj.draw();
	}
	class TrickOrTrickKidPumpkinBombClass : jjBEHAVIORINTERFACE {
		void onBehave(jjOBJ@ obj) {
			if (obj.state == STATE::START) {
				obj.curAnim += 1;
				obj.playerHandling = HANDLING::SPECIAL;
				obj.scriptedCollisions = true;
				obj.isFreezable = false;
				obj.light = 9;
				obj.lightType = LIGHT::PLAYER;
				if (obj.creatorType == CREATOR::OBJECT && jjObjects[obj.creatorID].eventID == obj.eventID) {
					obj.direction = jjObjects[obj.creatorID].direction;
					obj.deactivates = false;
					obj.state = STATE::BOUNCE;
					obj.bulletHandling = HANDLING::IGNOREBULLET;
				} else {
					obj.state = STATE::FALL;
					obj.isTarget = false;
					obj.bulletHandling = HANDLING::DESTROYBULLET;
				}
			}
			
			const int nearest = obj.findNearestPlayer(160 * 160);
			if (nearest >= 0)
				obj.frameID = ((jjPlayers[nearest].xPos > obj.xPos) == (obj.direction == 1)) ? 1 : 2;
			//else
			//	obj.frameID = 0;
			obj.curFrame = jjAnimations[obj.curAnim] + obj.frameID;
				
			if (obj.state == STATE::BOUNCE) {
				const jjOBJ@ creator = jjObjects[obj.creatorID];
				obj.xPos = creator.xPos;//just in case?
				if (creator.state == STATE::FREEZE) {
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::FROZEN);
				} else {
					obj.age += 8;
					const auto bounceHeight = (obj.age & 1024 == 0) ? 0 : jjSin(obj.age) * 40;
					obj.yPos = creator.yPos + jjAnimFrames[creator.curFrame].hotSpotY + 21 + (bounceHeight > 0 ? 0 : bounceHeight);
					jjDrawRotatedSpriteFromCurFrame(
						obj.xPos, obj.yPos,
						obj.curFrame,
						bounceHeight >= 0 ? int(sin(abs(jjSin(obj.age >> 1))) * 40 * obj.direction) : 0,
						obj.direction,1,
						creator.justHit == 0 ? SPRITE::NORMAL : SPRITE::SINGLECOLOR,
						15
					);
				}
			} else if (obj.state == STATE::DEACTIVATE) {
				obj.delete();
			} else if (obj.state == STATE::FALL) {
				if (jjMaskedPixel(int(obj.xPos), int(obj.yPos))) {
					if (obj.frameID != 0 && ++obj.counter >= 100) {
						explode(obj);
						return;
					}
					obj.ySpeed = 0;
					jjDrawSprite(obj.xPos, obj.yPos - 20, ANIM::AMMO, 1, (++obj.age) >> 1, -obj.direction);
				} else
					obj.yPos += obj.ySpeed += 0.05;
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction);
			}
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) {
			if (bullet is null) {
				if (play.hurt() && obj.state == STATE::FALL)
					explode(obj);
			}
			return true;
		}
		void explode(jjOBJ@ obj) const {
			jjSample(obj.xPos, obj.yPos, SOUND::COMMON_SPLUT);
			jjSample(obj.xPos, obj.yPos, SOUND::AMMO_BOEM1);
			obj.blast(100*100, false);
			obj.particlePixelExplosion(2);
			obj.particlePixelExplosion(2);
			obj.determineCurAnim(ANIM::AMMO, 3);
			obj.behavior = BEHAVIOR::EXPLOSION;
			obj.playerHandling = HANDLING::EXPLOSION;
		}
	}
	TrickOrTrickKidPumpkinBombClass TrickOrTrickKidPumpkinBomb();
	
	
	array<array<int>> PuppetStringLocationsX = {
		//front hand, front foot, head, back hand, back foot
		{3-29, 3-29, 0, 47-29, 47-29},
		{6, 44, 29, 55, 21},
		{6, 34, 23, 44, 24},
		{27, 28, 25, 20, 36},
		{57, 29, 35, 10, 57},
		{40, 26, 30, 20, 50},
		{18, 24, 26, 39, 44},
		{12, 13, 26, 50, 42},
		{23, 8, 34, 53, 42},
		{21, 5, 28, 55, 36},
		{12, 9, 25, 54, 30},
		{4, 52, 33, 62, 33},
		{5, 54, 34, 62, 27}
	}, PuppetStringLocationsY = {
		//front hand, front foot, head, back hand, back foot
		{3-11, 17-11, 0, 17-11, 3-11},
		{34, 54, 5, 24, 57},
		{28, 54, 3, 25, 49},
		{37, 51, 7, 32, 45},
		{21, 50, 9, 33, 27},
		{44, 58, 11, 45, 50},
		{51, 62, 10, 49, 63},
		{33, 55, 10, 41, 58},
		{21, 20, 15, 32, 57},
		{19, 44, 16, 32, 62},
		{19, 60, 11, 31, 63},
		{19, 54, 9, 26, 62},
		{35, 47, 5, 26, 60}
	};
	bool PuppetLocationsAdjusted = false;
	class Puppet : jjBEHAVIORINTERFACE {
		array<array<jjOBJ@>> Locations;
		int EventID;
		Puppet(uint8 eventID) {
			EventID = eventID;
			if (!PuppetLocationsAdjusted) {
				PuppetLocationsAdjusted = true;
				auto walkingFrame = jjAnimations[jjObjectPresets[eventID].curAnim].firstFrame;
				for (uint i = 1; i <= 12; ++i) {
					const jjANIMFRAME@ frame = jjAnimFrames[walkingFrame++];
					for (uint j = 0; j < 5; ++j) {
						PuppetStringLocationsX[i][j] += frame.hotSpotX;
						PuppetStringLocationsY[i][j] += frame.hotSpotY;
					}
				}
			}
			
			Initiate();
		}
			
		void Initiate() {
			for (int x = jjLayerWidth[4]; --x >= 0;)
				for (int y = jjLayerHeight[4]; --y >= 0;) {
					if (jjEventGet(x,y) == EventID) {
						jjParameterSet(x,y, -1,1, 1); //active
						const uint groupID = jjParameterGet(x,y, 1,8);
						if (Locations.length <= groupID)
							Locations.resize(groupID + 1);
						array<jjOBJ@>@ group = Locations[groupID];
						if (group.length == 0)
							group.insertLast(jjObjects[jjAddObject(EventID, 0,0, groupID,CREATOR::LEVEL, jjVOIDFUNCOBJ(PuppetHand))]);
						jjOBJ@ newLocation = jjObjects[jjAddObject(EventID, x * 32 + 15, y * 32 + 15, group[0].objectID, CREATOR::OBJECT, BEHAVIOR::INACTIVE)];
						newLocation.behavior = this;
						newLocation.behave();
						group.insertLast(newLocation);
					}
				}
			for (uint groupID = 0; groupID < Locations.length; ++groupID) {
				array<jjOBJ@>@ group = Locations[groupID];
				if (group.length < 2)
					continue;
				
				float averageX = 0, averageY = 0;
				const uint locationCount = group.length - 1;
				for (uint i = 1; i <= locationCount; ++i) {
					averageX += group[i].xOrg / locationCount;
					averageY += group[i].yOrg / locationCount;
				}
				group[0].xPos = averageX; //hand
				group[0].yPos = averageY;
			}
		}
		
		void PuppetHand(jjOBJ@ obj) {
			float targetX = 0, targetY;
			switch (obj.state) {
				case STATE::START:
					obj.curAnim += 2;
					obj.determineCurFrame();
					obj.state = STATE::TURN;
					//obj.isFreezable = true;
					obj.isTarget = true;
					//obj.deactivates = true;
					break;
				case STATE::TURN:
					{
						array<jjOBJ@> targets;
						array<jjOBJ@>@ group = Locations[obj.creatorID];
						for (uint i = 0; i < group.length; ++i)
							if (group[i].state == STATE::DELAYEDSTART && cast<jjBEHAVIORINTERFACE@>(group[i].behavior) is this)
								targets.insertLast(group[i]);
						if (targets.length == 0) {
							obj.particlePixelExplosion(72); //gray
							if (obj.var[5] == 1) { //alternate behavior
								obj.state = STATE::KILL;
								obj.behave(BEHAVIOR::GEMSTOMP);
							} else {
								obj.delete();
							}
							return;
						}
						obj.special = targets[jjRandom() % targets.length].objectID;
						if (targets.length == 1)
							obj.points = (group.length - 1) * 200;
					}
					obj.state = STATE::FLY;
					obj.playerHandling = HANDLING::PARTICLE;
				case STATE::FLY: {
					jjOBJ@ target = jjObjects[obj.special];
					targetX = target.xPos;
					targetY = target.yPos - 70;
					if (abs(obj.xPos - targetX) < 14 && abs(obj.yPos - targetY) < 14) {
						target.energy = 99;
						target.state = STATE::WALK;
						target.playerHandling = HANDLING::ENEMY;
						target.isFreezable = true; //hmmm
						//target.isTarget = true; //hmmm
						obj.state = STATE::ROTATE;
						obj.curFrame += 1;
						obj.playerHandling = HANDLING::ENEMY;
						obj.energy = 1;
					}
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::TRANSLUCENT);
				}
				case STATE::ROTATE:
					if (targetX == 0) { //didn't come here from FLY
						const jjOBJ@ target = jjObjects[obj.special];
						obj.xPos = targetX = target.xPos + jjSin(obj.counter * 3) * 14;
						targetY = target.yPos - 70 + jjCos(obj.counter += 3) * 9;
						jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.age = int(jjSin(obj.counter * 6) * 16));
						//obj.draw();
					}
					
					if (obj.xPos < targetX - 5) obj.direction = 3;
					else if (obj.xPos > targetX + 5) obj.direction = -3;
					else obj.direction = 0;
					obj.xPos += obj.direction;
					
					if (obj.yPos < targetY - 5) obj.yPos += 3;
					else if (obj.yPos > targetY + 5) { obj.yPos -= 3; obj.direction ^= 0x40; }
					break;
				case STATE::FREEZE:
					if (--obj.freeze <= 0)
						obj.state = obj.oldState;
					obj.draw();
					break;
				case STATE::DEACTIVATE:
					//nothing, ReloadPuppets will take care of everything later
					break;
				case STATE::KILL:
					obj.energy = (jjDifficulty <= 2) ? 1 : 2;
					obj.state = STATE::TURN;
					obj.curFrame -= 1;
					obj.playerHandling = HANDLING::PARTICLE;
					jjObjects[obj.special].state = STATE::KILL;
					break;
			}
		}
		
		void onBehave(jjOBJ@ obj) { //body
			switch (obj.state) {
				case STATE::START:
					{
						const int color = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0, 1);
						obj.special = jjAnimations[obj.curAnim + 5] + color;
						if (color == 1) {
							obj.curFrame += jjAnimations[obj.curAnim].frameCount;
							obj.curAnim += 1;
							obj.xSpeed = 2;
						}
					}
					obj.putOnGround(true);
					obj.state = STATE::DELAYEDSTART;
				case STATE::DELAYEDSTART:
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.special, obj.direction);
					return;
				case STATE::WALK:
					obj.xPos -= abs(jjSin(jjGameTicks << 2)) * obj.direction;
				case STATE::FREEZE:
					obj.energy = 99;
					obj.behave(BEHAVIOR::WALKINGENEMY, false);
					{
						const jjOBJ@ hand = jjObjects[obj.creatorID];
						const array<int>@
							HandX = @PuppetStringLocationsX[0],
							HandY = @PuppetStringLocationsY[0],
							LimbX = @PuppetStringLocationsX[obj.frameID + 1],
							LimbY = @PuppetStringLocationsY[obj.frameID + 1];
						for (uint i = 0; i < 5; ++i) {
							const auto handX = hand.xPos + jjCos(hand.age) * HandX[i] - jjSin(hand.age) * HandY[i];
							const auto handY = hand.yPos + jjCos(hand.age) * HandY[i] - jjSin(hand.age) * HandX[i];
							const auto limbX = obj.xPos + LimbX[i] * obj.direction;
							const auto limbY = obj.yPos + LimbY[i];
							const auto dX = limbX - handX;
							const auto dY = limbY - handY;
							const auto length = sqrt(dX * dX + dY * dY) / 50;
							if (length != 0)
								jjDrawRotatedSpriteFromCurFrame(
									handX, handY,
									hand.curFrame + 1,
									int(atan2(
										dX, dY
									) * 1024 / 6.283185307179586),
									1, length,
									SPRITE::SINGLECOLOR,64+i,
									i > 1 ? 4 : 3
								);
						}
					}
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, obj.state == STATE::FREEZE ? SPRITE::FROZEN : SPRITE::NORMAL);
					break;
				case STATE::KILL:
					obj.state = STATE::DONE;
					obj.playerHandling = HANDLING::PARTICLE;
					{
						const auto firstShardFrame = jjAnimations[obj.curAnim + 3].firstFrame;
						for (uint i = 0; i < 11; ++i)
							jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos, firstShardFrame + i, CREATOR::OBJECT, PuppetShard);
					}
					break;
			}
		}
	}
	void PuppetShard(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::SHARD, false);
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.creatorID, obj.direction);
	}
	void ReloadPuppets() {
		for (uint i = 32; i < 256; ++i) {
			jjBEHAVIORINTERFACE@ bi = cast<jjBEHAVIORINTERFACE@>(jjObjectPresets[i].behavior);
			if (bi !is null) {
				Puppet@ p = cast<Puppet@>(bi);
				if (p !is null) {
					p.Locations.resize(0);
					p.Initiate();
				}
			}
		}
	}
	
	array<int> NextSuckerTime(jjLocalPlayerCount, 0);
	array<const jjOBJ@> LastSuckerObject(jjLocalPlayerCount, null);
	array<bool> PlayerOnFire(jjLocalPlayerCount, false);
	class RingOfFire : jjBEHAVIORINTERFACE {
		void onBehave(jjOBJ@ obj) {
			if (obj.state == STATE::START) {
				obj.age = obj.special = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0, 3) * 128; //special: original angle
				obj.state = STATE::FLY;
				obj.var[2] = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 3, 3); //style
				if (jjDifficultyOrig >= 1)
					for (int i = 0; i < 2; ++i)
						obj.var[i] = jjAddObject(obj.eventID, 0,0, obj.objectID, CREATOR::OBJECT, function(h){h.behavior = RingOfFireHurtPoint; h.behave();});
			} else if (obj.state == STATE::DEACTIVATE) {
				if (jjDifficultyOrig >= 1)
					for (int i = 0; i < 2; ++i)
						jjObjects[obj.var[i]].delete();
				obj.deactivate();
			} else {
				//...
				switch (obj.var[2]) {
					case 0: //static
						break;
					case 1: //flip45
						obj.age = int(obj.special + jjSin(jjGameTicks << 1) * 128);
						break;
					case 2: //spin
						obj.age = (jjGameTicks << 2) + obj.special;
						break;
					case 3: //point at player
						{
							const int playerID = obj.findNearestPlayer(400 * 400);
							if (playerID >= 0) {
								const jjPLAYER@ play = jjPlayers[playerID];
								if (NextSuckerTime[play.localPlayerID] <= jjGameTicks || LastSuckerObject[play.localPlayerID] !is obj)
									obj.age = int(atan2(
										obj.yPos - play.yPos,
										play.xPos - obj.xPos
									) * 1024 / 6.283185307179586);
							}
						}
						break;
				}
				if (jjDifficultyOrig >= 1)
					for (int i = 0; i < 2; ++i) {
						jjOBJ@ hurt = jjObjects[obj.var[i]];
						hurt.xPos = obj.xPos + jjSin(obj.age + (i << 9)) * 32;
						hurt.yPos = obj.yPos + jjCos(obj.age + (i << 9)) * 32;
					}
			}
		}
		void onDraw(jjOBJ@ obj) {
			jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curAnim+1, obj.age, 1,1, SPRITE::NORMAL,0, 3);
			if (jjDifficultyOrig >= 1) {
				const auto cosa = jjCos(obj.age), sina = jjSin(obj.age);
				for (int i = 0; i < 1024; i += !jjLowDetail ? 64 : 128) {
					const auto x = jjSin(i) * 13, y = jjCos(i) * 32;
					jjDrawSprite(
						obj.xPos + cosa * x + sina * y,
						obj.yPos + cosa * y - sina * x,
						ANIM::AMMO, 13, (jjGameTicks >> 3) + i,
						1,
						SPRITE::NORMAL, 0,
						i < 512 ? 4 : 3
					);
				}
			}
				//or anim 55?
			jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curAnim, obj.age);
			//obj.draw();
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) {
			if (bullet is null && play.isLocal) {// && play.curAnim - jjAnimSets[play.setID] != RABBIT::HURT && play.blink == 0) {
				if (abs(play.invincibility) < 20)
					play.invincibility = (play.invincibility <= 0) ? -20 : 20;
				if (NextSuckerTime[play.localPlayerID] <= jjGameTicks || LastSuckerObject[play.localPlayerID] !is obj) {
					NextSuckerTime[play.localPlayerID] = jjGameTicks + 25;
					@LastSuckerObject[play.localPlayerID] = obj;
					PlayerOnFire[play.localPlayerID] = false;
					play.xPos = obj.xPos;
					play.yPos = obj.yPos;
					play.suckerTube(int(jjCos(obj.age) * 10), int(jjSin(obj.age) * -17), false);
					play.helicopter = 0;
					play.alreadyDoubleJumped = false;
					if (jjDifficultyOrig > 0)
						jjSample(obj.xPos, obj.yPos, SOUND::AMMO_FIREGUN1A);
					else
						jjSample(obj.xPos, obj.yPos, SOUND::COMMON_BURN, 0, 50000); //trigsample
				}
			}
			return true;
		}
	}
	class RingOfFireHurtPointClass : jjBEHAVIORINTERFACE {
		void onBehave(jjOBJ@ obj) {
			if (obj.state == STATE::START) {
				obj.curFrame -= 4;
				obj.playerHandling = HANDLING::PICKUP;
				obj.scriptedCollisions = true;
				obj.deactivates = false;
			}
			obj.state = STATE::FLY; //reusable
			//obj.draw();
		}
		bool onObjectHit(jjOBJ@, jjOBJ@, jjPLAYER@ play, int) {
			if (play.shieldType != SHIELD::FIRE && play.hurt(1)) {
				NextSuckerTime[play.localPlayerID] = jjGameTicks + 70;
				PlayerOnFire[play.localPlayerID] = true;
			}
			return true;
		}
	}
	RingOfFireHurtPointClass RingOfFireHurtPoint;
}