Downloads containing Jazz1Enemies v05.asc

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Jazz of the JungleFeatured Download Violet CLM Single player 8.9 Download file
Jazz 1 Enemies Violet CLM Other N/A Download file

File preview

/*************************

										Jazz1Enemies v05.asc
										version 0.5

API:



	Jazz1::Enemy@ Jazz1::MakeEnemy(uint8 eventID, Jazz1::Enemies enemyID, bool useTrueColor = false, float scale = 1.f, uint resizeMethod = Resize::Method::NearestNeighbor)
		This is the main function and is intended to be called somewhere in the onLevelLoad hook. Its purpose is to convert one of the events in the level, as specified by the eventID argument, to be an enemy from Jazz 1 instead of whatever it originally was. For example:
		
			Jazz1::MakeEnemy(OBJECT::HELMUT, Jazz1::Enemies::Medivo_Helmut);
			
		This function modifies various properties of jjObjectPresets[eventID], in particular jjOBJ::behavior, which is set to a new jjBEHAVIORINTERFACE instance. The value of the "eventID" argument is totally up to you (although using an OBJECT::Object constant is recommended); possible values for the "enemyID" argument are listed shortly below this comment in the "Enemies" enum, starting with "Diamondus_BumblingBee".
			
		The third argument, "useTrueColor", lets you choose which color model to use for drawing this particular enemy:
			false (default):
				The enemy will be limited to using colors that appear in the standard sprite palette. It will look the same in both 8-bit and 16-bit color, and it will be recolored to fit the atmosphere in tilesets with noticeably altered sprite palettes, e.g. Swamps or Glowee. However, it may not look exactly like the original Jazz 1 sprites, depending on how closely the original colors were or were not similar to those available in Jazz 2's standard sprite palette.
			true:
				In 8-bit color this option will have no effect, but in 16-bit color the enemy will be drawn in an approximation of palette-independent 24-bit color mode. It should look much closer, if not identical, to the original Jazz 1 sprites; however, it may therefore not match the atmosphere of the tileset, and if you plan to make any calls to jjPAL::apply() later, you should use the TrueColor::EnableCaching function (see TrueColor v13.asc's documentation for details).
				
		Finally there are two more arguments, "scale" and "resizeMethod", for in case you want to make the Jazz 1 enemy sprites somewhat larger to better match the other sprites around them. The "scale" argument should be self-explanatory and defaults to 1.f (no resizing at all); see Resize v11.asc's documentation for a list of options for the "resizeMethod" argument. The object and/or its bullets may also be instructed to move faster or slower to better match larger or smaller sprites, using the SetEnemySpeedsAreScalable and SetBulletSpeedsAreScalable methods described below.
		
		That one function call, once per enemy you want to add to your level, is all you need to do to use this library, besides the "#include Jazz1Enemies v05.asc" line (see the "Packaging Instructions" section below for more details). There are also some options available to you, however. Jazz1::MakeEnemy returns a handle to an object of class Jazz1::Enemy, which has a number of methods allowing you to slightly edit how that enemy behaves. Each of those methods returns another handle to the same object, allowing you to chain multiple method calls on the same line as the original Jazz1::MakeEnemy call, e.g.:
		
			Jazz1::MakeEnemy(OBJECT::NORMTURTLE, Jazz1::Enemies::Diamondus_TurtleGoon).SetDirection(Jazz1::Directions::Random).SetSpeed(6);
			
		Many of the object methods are available to certain enemies but not others; for example, SetFireDelay is not generally useful for enemies that do not fire any bullets. If you make such an inappropriate method call, you will see a warning to this effect in your chatlog (if the [General]AngelscriptDebug setting is enabled in your plus.ini) but no behavior change will occur.
		
		Note that each Jazz1::Enemy instance is unique to a Jazz1::MakeEnemy call, even if you use the same value for more than one enemyID argument. To have one event place Tubelectric blasters on the floor and another event place them on the ceiling, for instance, you could write:
		
			Jazz1::MakeEnemy(OBJECT::APPLE, Jazz1::Enemies::Tubelectric_Blaster).SetDirection(Jazz1::Directions::Right);
			Jazz1::MakeEnemy(OBJECT::BANANA, Jazz1::Enemies::Tubelectric_Blaster).SetDirection(Jazz1::Directions::Left);
		
		
		
	List of Jazz1::Enemy methods (the return type for the Set* methods is always Jazz1::Enemy@, so you can chain these methods indefinitely):
		SetSpeed(float speed)
			Replaces the enemy's default movement speed with a new one of your choice. Different enemies have different default speeds, so you'll have to play around with this number a bit in order to find what feels best for you.
			
		SetSpeedBasedOnParameter(uint offset, uint length, float modifier = 1.0f)
			Instead of its movement speed being a constant, the enemy will base its speed on an event parameter you specified in JCS. (This will require an appropriately edited JCS.ini entry for that eventID). The enemy will get a parameter value based on the "offset" and "length" arguments, familiar from the global "jjParameterGet" function; add 1 to that value (so that the enemy never moves at speed 0); and then multiply that value by the "modifier" argument. For example, if "length" is 2 and "modifier" is 0.5, then any instance of that enemy you place in the level will have its movement speed be either 0.5, 1.0, 1.5, or 2.0, depending on what parameter you set in the level.
			
		SetDirection(int offsetOrConstant)
			Four constant values are provided for use with this method, to be found in the Jazz1::Directions enum: Left, Right, FaceJazz, and Random. Mostly this method specifies which direction the enemy should face when it is first spawned in the level, but a handful of enemies will check it constantly, so e.g. a Nippius_SnowGoon enemy would be able to turn around to face the nearest local player at all times if passed Jazz1::Directions::FaceJazz.
			If you pass an unsigned integer value instead of a Jazz1::Directions constant, that will make the enemy base its direction on an event parameter instead, specifically a parameter whose length is 1 and whose offset is the value you passed, just like e.g. springs or CTF bases.
			
		SetFireDelay(uint delay)
			How many gameticks (70 per second) should pass after the enemy fires a bullet before it fires another one. Each enemy has its own default delay time but you can replace it.
			For hopping enemies (e.g. Marbelara_Firebomb), this sets the number of ticks the enemy waits at its starting position between hops.
			
		SetUsesJJ2StyleDeathAnimation(bool setTo)
			By default, all enemies use more or less their original death animations, e.g. a Tubelectric_Spark enemy explodes into yellow shards. If you pass true to this method, that enemy will instead die by bursting into particles appropriate to a JJ2 enemy dying from whatever means (regular bullet, toaster bullet, physical attack, etc.) was used to kill it.
			(This is a PURELY visual change. Either way the player will get points and potentially a pickup for killing the enemy.)
			
		SetWalkingEnemyCliffReaction(Jazz1::CliffReaction)
			All enemies that walk back and forth on the floor share their basic code, and this method lets you choose how they should react when they run out of floor to walk on. The available values of the Jazz1::CliffReaction enum are TurnAround (e.g. Diamondus_TurtleGoon), Fall (e.g. Medivo_Helmut), and Careen (not found in Jazz 1, but perhaps a slightly more attractive option than falling straight down).
			
		SetEnemySpeedsAreScalable(bool setTo)
		SetBulletSpeedsAreScalable(bool setTo)
			Each enemy's movement speed, as well as the movement speeds of its bullets if any, assumes the sprites will be drawn at their original size. If you pass a value other than 1.f for the "scale" argument of MakeEnemy, however, this may look wrong, so there are a couple methods to have the enemies and/or bullets' speeds scale as well. Both default false.
			
		SetDeathSound(SOUND::Sample sample)
		SetBulletFireSound(SOUND::Sample sample)
		SetBulletExplosionSound(SOUND::Sample sample)
			These methods let you choose which sounds are played when (respectively) the enemy dies, the enemy fires a bullet, or a bullet fired by the enemy explodes.
			
		SetPointsFormula(Jazz1::PointsFormula pointsFormula)
			By default (Jazz1::PointsFormula::Original), killing an enemy will give you the same number of points it did in JJ1. This is always a multiple of 10, often 20 or 30. However, normal JJ2 objects only ever give points in multiples of 50. So you may apply a different rule to this enemy by using the alternate options Jazz1::PointsFormula::MultiplyByFive (e.g. 20 would become 100) or Jazz1::PointsFormula::RoundUpToMultipleOfFifty (e.g. 20 would become 50).
		
		uint GetFirstFrame(bool useTrueColor = false) const
			The only Get*, not Set*, method. If you want to use the sprites from one or more Jazz 1 Enemies, but not the behavior class this library provides for it, this method will give you the first frame of the enemy's animset (as an index to jjAnimFrames, suitable for jjOBJ::curFrame usage) for you to use however you wish. You can look at the corresponding .png image for a guide to what the enemy's various frames are, with the leftmost frame in the image the one whose ID GetFirstFrame specifically returns you. For example, "Jazz1::MakeEnemy(OBJECT::APPLE, Jazz1::Enemies::Technoir_TanketyTankTank).GetFirstFrame() + 5" will get you the Technoir_TanketyTankTank's rightward-facing pineapple missile sprite.
			If you pass true for the "useTrueColor" argument, you'll get the first frame again, but as formatted for use by the TrueColor library, e.g. "Jazz1::MakeEnemy(OBJECT::APPLE, Jazz1::Enemies::Technoir_TanketyTankTank, true).GetFirstFrame(true) + 5 * TrueColor::NumberOfFramesPerImage". The return value if you pass "true" to GetFirstFrame's "useTrueColor" argument but _not_ to MakeEnemy's "useTrueColor" argument is undefined.
			
	Additionally, you are welcome to edit various of a jjObjectPreset entry's properties in accordance with their usual meanings, including jjOBJ::energy, jjOBJ::points, jjOBJ::isFreezable, jjOBJ::isTarget, jjOBJ::light, and jjOBJ::lightType; however, the MakeEnemy call will assign to many of them the default values for that enemy, so you should not edit those properties for a given preset until AFTER calling MakeEnemy on it.
	
	When dealing with active objects, rather than presets, the jjKillObject function is fully supported. (As are various others, but that seemed most worthy of note.)
			
	These enemies should work just about as well online (in SP/Coop servers) as any other enemies do, provided the same code is run by all players in the server.
	
	
	
	
	
	
Packaging Instructions:


	In the most basic case, your script needs to include only one line:
		#include "Jazz1Enemies v05.asc"
	This will automatically work in multiplayer servers; this library automatically inserts enough preprocessor instructions for clients to know to download the relevant files.
	
	If you are packaging a .zip archive for a level/mutator using this library, you will need to include all four of these files, even if you are not actively using the Resize and/or TrueColor libraries for any of the enemies in this level:
		Jazz1Enemies v05.asc
		Jazz1Enemies v05.j2a
		Resize v11.asc
		TrueColor v13.asc
	
	
	If you do make one or more calls to Jazz1::MakeEnemy with the "useTrueColor" argument equalling true, however, you will need to handle the .png image for each such call manually. It must be included in any .zip archive, and in the event that you want the script to work online, you will need to write a "#pragma require" line specifically for that .png.

	For example, suppose you have two calls to Jazz1::MakeEnemy, one of them passing "useTrueColor" as false and one passing it as true:
		Jazz1::MakeEnemy(OBJECT::BUMBEE, Jazz1::Enemies::Diamondus_BumblingBee, false);
		Jazz1::MakeEnemy(OBJECT::NORMTURTLE, Jazz1::Enemies::Diamondus_TurtleGoon, true);
	In this case, Diamondus_BumblingBee requires no special treatment (because it is not using True Color), but your script should include both these lines:
		#include "Jazz1Enemies v05.asc"
		#pragma require "Jazz1Enemies_Diamondus_TurtleGoon.png"
	And your .zip archive should include these five files:
		Jazz1Enemies v05.asc
		Jazz1Enemies v05.j2a
		Jazz1Enemies_Diamondus_TurtleGoon.png
		Resize v11.asc
		TrueColor v13.asc
		
		
	The pattern for True Color image filenames is perfectly regular: the name of the enemy, with "Jazz1Enemies_" as the prefix, followed by the file extension ".png". So:
		Jazz1::Enemies::Letni_BugCeiling -> Jazz1Enemies_Letni_BugCeiling.png
		Jazz1::Enemies::Marbelara_Schwarzenguard -> Jazz1Enemies_Marbelara_Schwarzenguard.png
		Jazz1::Enemies::Battleships_Generator -> Jazz1Enemies_Battleships_Generator.png
	...etc.
		
*************************/

namespace Jazz1 {
	enum Enemies { _Misc_Anims,
	//Full list of Enemy IDs for passing as the second argument to Jazz1::MakeEnemy (but remember to precede any name with "Jazz1::Enemies::"):
		Diamondus_BumblingBee, Diamondus_TurtleGoon,
		Tubelectric_BlasterHorizontal, Tubelectric_BlasterVertical, Tubelectric_Spark, Tubelectric_SparkBarrier, //for BlasterVertical, right=on floor, left=on ceiling
		Medivo_GhostRapierHorizontal, Medivo_GhostRapierVertical, Medivo_Helmut,
		Letni_Bug, Letni_BugCeiling, Letni_ElecBarrier,
		Technoir_MiniMine, Technoir_Misfire, Technoir_TanketyTankTank,
		Orbitus_BeholderPurple, Orbitus_BeholderSilver, Orbitus_SilverSnake,
		Fanolint_FlyFlower, Fanolint_PottedPlant, Fanolint_SuperTankety,
		Scraparap_GunnerDrone, Scraparap_LaunchCart, Scraparap_RoboTurtleDrone,
		Megairbase_Doofusguard, Megairbase_Missile, Megairbase_SuperSpark, //Megairbase also essentially reuses Tubelectric_Spark, but that does not a separate enum value warrant
		Turtemple_JeTurtle, Turtemple_ScorpWeenie,
		Nippius_SkatePen, Nippius_SkiTurtle, Nippius_SnowGoon,
		Jungrock_JetSnake, Jungrock_RedBuzzer, Jungrock_YellowBuzzer,
		Marbelara_Drageen, Marbelara_Firebomb, Marbelara_Schwarzenguard,
		Sluggion_Dragoon, Sluggion_RedBat, Sluggion_Sluggi,
		Dreempipes_Minite, Dreempipes_Overgrown, Dreempipes_TerrapinSwimmer,
		Pezrox_ClammyHorizontal, Pezrox_ClammyVertical, Pezrox_GreenSnake, //for ClammyVertical, right=down, left=up
		Crysilis_GoldenBounceSpike, Crysilis_LooGuard,
		Battleships_ArmorDoofi, Battleships_BounceSpike, Battleships_Generator, Battleships_SuperBee,
		/*Exoticus_???,
		Industrius_???,
		Muckamok_???,
		Raneforus_???,
		Stonar_???,
		Deckstar_???,
		Ceramicus_???,
		Deserto_???,
		Lagunicus_???,*/
		Holidaius_BlueDog, Holidaius_Devil, Holidaius_HandHorizontal, Holidaius_HandVertical, Holidaius_SkiTurtle, Holidaius_SnowMonkey, //for HandVertical, right=on floor, left=on ceiling
		//Candion_Rat, Bloxonius_GreenBaron, Bloxonius_RaggedyAnne, Bloxonius_RedBaron
		
		LAST
	};
	
	//Everything below is subject to change and need not therefore be read by users; only the above enum and API description should be taken as promises.
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	const array<string> _enemyNames = {
		"Diamondus_BumblingBee", "Diamondus_TurtleGoon",
		"Tubelectric_BlasterHorizontal", "Tubelectric_BlasterVertical", "Tubelectric_Spark", "Tubelectric_SparkBarrier",
		"Medivo_GhostRapierHorizontal", "Medivo_GhostRapierVertical", "Medivo_Helmut",
		"Letni_Bug", "Letni_BugCeiling", "Letni_ElecBarrier",
		"Technoir_MiniMine", "Technoir_Misfire", "Technoir_TanketyTankTank",
		"Orbitus_BeholderPurple", "Orbitus_BeholderSilver", "Orbitus_SilverSnake",
		"Fanolint_FlyFlower", "Fanolint_PottedPlant", "Fanolint_SuperTankety",
		"Scraparap_GunnerDrone", "Scraparap_LaunchCart", "Scraparap_RoboTurtleDrone",
		"Megairbase_Doofusguard", "Megairbase_Missile", "Megairbase_SuperSpark",
		"Turtemple_JeTurtle", "Turtemple_ScorpWeenie",
		"Nippius_SkatePen", "Nippius_SkiTurtle", "Nippius_SnowGoon",
		"Jungrock_JetSnake", "Jungrock_RedBuzzer", "Jungrock_YellowBuzzer",
		"Marbelara_Drageen", "Marbelara_Firebomb", "Marbelara_Schwarzenguard",
		"Sluggion_Dragoon", "Sluggion_RedBat", "Sluggion_Sluggi",
		"Dreempipes_Minite", "Dreempipes_Overgrown", "Dreempipes_TerrapinSwimmer",
		"Pezrox_ClammyHorizontal", "Pezrox_ClammyVertical", "Pezrox_GreenSnake",
		"Crysilis_GoldenBounceSpike", "Crysilis_LooGuard",
		"Battleships_ArmorDoofi", "Battleships_BounceSpike", "Battleships_Generator", "Battleships_SuperBee",
		//ABC
		"Holidaius_BlueDog", "Holidaius_Devil", "Holidaius_HandHorizontal", "Holidaius_HandVertical", "Holidaius_SkiTurtle", "Holidaius_SnowMonkey"
		//"Candion_Rat", "Bloxonius_GreenBaron", "Bloxonius_RaggedyAnne", "Bloxonius_RedBaron"
	};
	
	enum Directions { Left = -4, Right = -3, FaceJazz = -2, Random = -1 };
	enum CliffReaction { TurnAround, Fall, Careen };
	enum PointsFormula { Original, MultiplyByFive, RoundUpToMultipleOfFifty };
	
	enum _objVar { AnimCounter, DirectionCurrent, FireDelayCounter };
	enum _causeOfDeath { Bullet, OrangeShards, BlueShards, GrayShards, PhysicalAttack, FrozenBullet, FrozenPhysicalAttack, AlreadyPerformedAnimation};
	const float _levelWidth = jjLayerWidth[4] * 32;
	const float _levelHeight = jjLayerHeight[4] * 32;
	
	dictionary _animSets;
	
	bool _trueColorHasProcessedPalette = false;
	
	int _getParameter(jjOBJ@ obj, int offset, int length) {
		return jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, offset, length);
	}
	bool _maskedPixelFloat(float x, float y) {
		return jjMaskedPixel(int(x), int(y));
	}
	const AnimSetDetails@ _getAnimSet(Enemies enemyID, float scale = 1.f, string name = "", uint resizeMethod = Resize::Method::AdvMAME2x) {
		const bool truecolor = !name.isEmpty();
		const string key = enemyID + "-" + scale;// + "-" + truecolor;
		AnimSetDetails@ result;
		bool setHasBeenLoadedBefore;
		if (!(setHasBeenLoadedBefore = _animSets.get(key, @result))) {
			@_animSets[key] = @result = AnimSetDetails();
			result.animID = jjAnimSets[TrueColor::FindCustomAnim()].load(enemyID, "Jazz1Enemies v05.j2a").firstAnim;
			result.firstFrame = jjAnimations[result.animID].firstFrame;
			
			{
				uint animID = result.animID;
				while (jjAnimations[animID++].frameCount != 0)
					result.animCount += 1; //this number is not directly stored anywhere, so I have to infer it from when I run out of used animations
			}
		}
			
		if (truecolor && result.firstFrameTrueColor == 0) { //images haven't been generated for this enemy/scale combo yet.
			if (!_trueColorHasProcessedPalette) {
				TrueColor::ProcessPalette();
				_trueColorHasProcessedPalette = true;
			}
			
			array<array<TrueColor::Coordinates>> _trueColorCoordinates = {array<TrueColor::Coordinates>(0)}; //construct the TrueColor animsets as single flat animations, no matter the structures of the animsets from Jazz1Enemies.j2a, because all I really care about is frame offset
			
			uint leftPositionOfAnimFrameInSpriteSheet = 0;
			for (uint i = 0; i < result.animCount; ++i) { //generate an array<TrueColor::Coordinates> based on the animset's animations' frames' properties
				const jjANIMATION@ animation = jjAnimations[result.animID + i];
				for (uint j = 0; j < animation.frameCount; ++j) {
					const jjANIMFRAME@ animFrame = jjAnimFrames[animation + j];
					_trueColorCoordinates[0].insertLast(TrueColor::Coordinates(
						leftPositionOfAnimFrameInSpriteSheet, 0,
						animFrame.width, animFrame.height,
						animFrame.hotSpotX, animFrame.hotSpotY/*, //the other properties aren't needed
						animFrame.gunSpotX, animFrame.gunSpotY,
						animFrame.coldSpotX, animFrame.coldSpotY*/
					));
					leftPositionOfAnimFrameInSpriteSheet += animFrame.width; //the next sprite on the spritesheet, if any, will be placed immediately to the right of this one
				}
			}
			
			const ANIM::Set trueColorAnimSet = TrueColor::FindCustomAnim();
			auto bitmap = TrueColor::Bitmap("Jazz1Enemies_" + name + ".png");
			if (scale != float(int(scale)) || (resizeMethod & (Resize::Flags::_firstFlag - 1)) >= Resize::Method::_firstInvolvingFindNearestColor)
				bitmap.generateAlphaChannel();
			Resize::AllocateAndResizeTrueColorSpriteSheet(
				trueColorAnimSet,
				bitmap,
				_trueColorCoordinates,
				scale, resizeMethod
			);
			result.firstFrameTrueColor = jjAnimations[jjAnimSets[trueColorAnimSet]];
		}
			
		if (!setHasBeenLoadedBefore && scale != 1.f)
			for (uint i = 0; i < result.animCount; ++i)
				Resize::Resize(jjAnimations[result.animID + i], scale, resizeMethod, scale);
		return @result;
	}
	
	void _explosion(jjOBJ@ obj) {
		if (obj.ySpeed != 0) {
			obj.xPos += obj.xSpeed;
			obj.yPos += obj.ySpeed += 0.125f;
		}
		if (jjRandom() & 7 < uint(obj.ySpeed == 0 ? 3 : 5)) { //advance frame
			jjANIMATION@ anim = jjAnimations[obj.curAnim];
			if(++obj.frameID >= int(anim.frameCount)) {
				obj.frameID = 0;
				if (++obj.counterEnd >= obj.creatorID) { //_killAnimRepetitionCounts
					obj.delete();
					return;
				}
			}
			obj.curFrame = anim.firstFrame + obj.frameID;
		}
		jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame);
	}
	
	enum _bulletDirection { Left = -1, Either = 0, Right = 1 };
	class _bulletPreferences {
		private float xSpeed, ySpeed, yAcc;
		private uint animID, frameID;
		_bulletDirection direction;
		_bulletPreferences(){}//array purposes
		_bulletPreferences(float x, float y, uint a, uint f, _bulletDirection d, float ya = 0) { xSpeed = x; ySpeed = y; animID = a; frameID = f; direction = d; yAcc = ya / 4;}
		jjOBJ@ fire(const jjOBJ@ obj, int sound, float scaleSpeed, float scaleSprite) const {
			int x1, x2, y1, y2;
			{ //jjOBJ::fireBullet doesn't work with vertically flipped frames, so we'll have to figure out on our own where the bullet object needs to be spawned
				const jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
				if (obj.direction >= 0) {
					x1 = frame.hotSpotX;
					x2 = frame.gunSpotX;
				} else { //horizontally flipped
					x2 = frame.hotSpotX;
					x1 = frame.gunSpotX;
				}
				if (obj.direction != SPRITE::FLIPV && obj.direction != SPRITE::FLIPHV) {
					y1 = frame.hotSpotY;
					y2 = frame.gunSpotY;
				} else { //vertically flipped
					y2 = frame.hotSpotY;
					y1 = frame.gunSpotY;
				}
			}
			jjOBJ@ bullet = jjObjects[jjAddObject(
				OBJECT::BULLET,
				obj.xPos + x1 - x2,
				obj.yPos + y1 - y2,
				obj.objectID, CREATOR::OBJECT,
				BEHAVIOR::INACTIVE
			)];
			bullet.xSpeed = xSpeed * scaleSpeed;
			bullet.ySpeed = ySpeed * scaleSpeed;
			bullet.yAcc = yAcc * scaleSpeed;
			bullet.curFrame = jjAnimations[jjObjectPresets[obj.eventID].curAnim + animID] + frameID;
			bullet.behavior = _bullet;
			bullet.special = sound;
			bullet.killAnim = _getAnimSet(Enemies::_Misc_Anims, scaleSprite).animID + 1;
			return bullet;
		}
	}
	void _bullet(jjOBJ@ obj) {
		if (obj.state == STATE::START) {
			obj.state = STATE::FLY;
			obj.playerHandling = HANDLING::ENEMYBULLET;
			obj.animSpeed = 1;
			obj.lightType = LIGHT::POINT2;
		}
		obj.xPos += obj.xSpeed;
		obj.yPos += obj.ySpeed += obj.yAcc;
		if (obj.curAnim == 0 || jjColorDepth == 8)
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame);
		else
			TrueColor::DrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curAnim);
		if (obj.state == STATE::EXPLODE || obj.xPos < 0 || obj.yPos < 0 || obj.xPos >= _levelWidth || obj.yPos >= _levelHeight || _maskedPixelFloat(obj.xPos, obj.yPos)) {
			obj.playerHandling = HANDLING::EXPLOSION;
			obj.curAnim = obj.killAnim;
			obj.behavior = _explosion;
			obj.creatorID = 1; //repetitions
			obj.ySpeed = 0; //don't move
			obj.counterEnd = 0;
			if (obj.special >= 0)
				jjSample(obj.xPos, obj.yPos, SOUND::Sample(obj.special));
		}
	}
	
	class EnemyArguments {
		jjOBJ@ preset; Enemies enemyID; bool tc; float scale; uint resizeMethod;
		EnemyArguments(jjOBJ@ p, Enemies e, bool t, float s, uint r) { @preset = p; enemyID = e; tc = t; scale = s; resizeMethod = r; }
	}
	class AnimSetDetails {
		uint animID, animCount, firstFrame, firstFrameTrueColor;
	}
	abstract class Enemy : jjBEHAVIORINTERFACE {
		private string _behaviorName;
		private bool _useTrueColor;
		protected float _scale;
		private uint _firstFramePaletted, _firstFrameTrueColor;
		
		protected array<int8>@ _animFrames = array<int8>(0), _fireFrames = null;
		protected uint _fireAnimID = 0;
		protected jjOBJ@ preset;
		
		Enemy(const EnemyArguments &in arg) {
			_behaviorName = _enemyNames[arg.enemyID - 1];
			@preset = arg.preset;
			auto animDetails = _getAnimSet(arg.enemyID, arg.scale, arg.tc ? _behaviorName : "", arg.resizeMethod);
			preset.curAnim = animDetails.animID;
			_firstFramePaletted = preset.curFrame = animDetails.firstFrame;
			_firstFrameTrueColor = animDetails.firstFrameTrueColor;
			_scale = arg.scale;
			_useTrueColor = arg.tc;
			
			for (uint i = 0; i < jjAnimations[preset.curAnim].frameCount; ++i) //create a default _animFrames array that repeats every frame in the animation exactly once and in linear order
				_animFrames.insertLast(i);
			
			preset.behavior = this;
			preset.playerHandling = HANDLING::SPECIAL;
			preset.bulletHandling = HANDLING::DETECTBULLET;
			preset.scriptedCollisions = true;
			preset.isTarget = true;
			preset.isFreezable = true;
			preset.isBlastable = true;
			preset.triggersTNT = true;
			preset.causesRicochet = false;
			preset.deactivates = true;
			preset.direction = 1;
			preset.state = STATE::START;
			preset.freeze = 0;
			preset.energy = preset.points = preset.animSpeed = 0; //these should be overridden
			preset.killAnim = -1; //first misc anim
			preset.var[_objVar::AnimCounter] = 0;
			preset.var[_objVar::FireDelayCounter] = 0;
		}
		void onBehave(jjOBJ@ obj) {
			if (obj.state == STATE::DEACTIVATE)
				obj.deactivate();
			else if (obj.state == STATE::KILL) {
				if (_useJJ2DeathAnimation) {
					switch (_died) {
						case _causeOfDeath::Bullet:
							obj.particlePixelExplosion(0);
							break;
						case _causeOfDeath::OrangeShards:
							jjSample(obj.xPos, obj.yPos, SOUND::COMMON_BURN);
							obj.particlePixelExplosion(1);
							break;
						case _causeOfDeath::BlueShards:
							jjSample(obj.xPos, obj.yPos, SOUND::COMMON_BURN);
							obj.particlePixelExplosion(32);
							break;
						case _causeOfDeath::GrayShards:
							jjSample(obj.xPos, obj.yPos, SOUND::COMMON_BURN);
							obj.particlePixelExplosion(72);
							break;
						case _causeOfDeath::PhysicalAttack:
							obj.particlePixelExplosion(2);
							jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::COMMON_SPLAT1 + (jjRandom() & 3)));
							break;
						case _causeOfDeath::FrozenBullet:
							obj.unfreeze(0);
							break;
						case _causeOfDeath::FrozenPhysicalAttack:
							obj.unfreeze(1);
							break;
						case _causeOfDeath::AlreadyPerformedAnimation:
							break;
					}
				} else { //Jazz1-style
					const auto killAnim = _getPresetRelativeAnimID(int(obj.killAnim));
					for (int i = 0; i < 7; ++i) {
						jjOBJ@ shard = jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos, _killAnimRepetitionCounts)];
						shard.curAnim = killAnim;
						shard.behavior = _explosion;
						shard.counterEnd = 0;
						if (_killAnimExplosion) {
							shard.behave(BEHAVIOR::SHARD); //get some random directions
							shard.xSpeed *= _scale;
							shard.ySpeed *= _scale;
							shard.ySpeed += 0.0001; //non-zero
						} else
							break; //only one
					}
				}
				if (_deathSound >= 0)
					jjSample(obj.xPos, obj.yPos, SOUND::Sample(_deathSound));
				obj.delete();
			} else if (obj.state == STATE::FREEZE) {
				if (obj.freeze-- <= 1) {
					obj.freeze = 0;
					obj.state = obj.oldState;
				}
			} else {
				if (obj.state == STATE::START) {
					if (_supportsSpeed) {
						//we're using xAcc exclusively, instead of xSpeed, because xAcc can't get changed by TNT
						if (_speedParamOffset >= 0)
							obj.xAcc = (_getParameter(obj, _speedParamOffset, _speedParamLength) + 1) * _speed;
						else
							obj.xAcc = _speed;
						obj.xAcc *= _scaledObjectSpeed;
					}
					if (jjDifficulty >= 3) //turbo
						obj.energy += 1;
				}
				
				if (_supportsFire) {
					if (obj.counterEnd > _fireDelay) { //showing fire animation
						obj.var[_objVar::AnimCounter] = obj.var[_objVar::AnimCounter] + 1;
						if (obj.var[_objVar::AnimCounter] > obj.animSpeed) {
							obj.var[_objVar::AnimCounter] = 0;
							if (uint(++obj.frameID) >= _fireFrames.length) {
								_fireBullets(obj);
								obj.counterEnd = 0;
								return;
							}
						}
						obj.curFrame = jjAnimations[_getPresetRelativeAnimID(_fireAnimID)].firstFrame + _fireFrames[obj.frameID];
						return;
					} else if (++obj.counterEnd > _fireDelay) { //start firing
						if (_fireFrames !is null && _fireFrames.length > 0) {
							obj.var[_objVar::AnimCounter] = 0;
							obj.frameID = 0;
							obj.curFrame = jjAnimations[_getPresetRelativeAnimID(_fireAnimID)].firstFrame + _fireFrames[obj.frameID];
							return;
						} else {
							_fireBullets(obj);
							obj.counterEnd = 0;
							//fall through...
						}
					}
				}
				
				myBehave(obj);
				
				jjANIMATION@ anim = jjAnimations[obj.curAnim];
				obj.var[_objVar::AnimCounter] = obj.var[_objVar::AnimCounter] + 1;
				obj.curFrame = anim.firstFrame + _animFrames[(((obj.var[_objVar::AnimCounter] >> 1) / (obj.animSpeed+1)) % _animFrames.length)];
				
				if (_flipSpriteWhenMovingLeft) //otherwise always face right
					obj.direction = obj.var[_objVar::DirectionCurrent];
			}
		}
		protected void myBehave(jjOBJ@ obj) const { obj.state = STATE::IDLE; } //here to be overridden
		
		protected void _drawBodyFrame(const jjOBJ@ obj, float xPos, float yPos, uint frameID, int direction, int8 layer = 4) const {
			if (!_useTrueColor || jjColorDepth == 8 || obj.freeze != 0 || obj.justHit != 0)
				jjDrawSpriteFromCurFrame(
					xPos, yPos, frameID, direction,
					(obj.freeze == 0) ? (obj.justHit == 0) ? SPRITE::NORMAL : SPRITE::SINGLECOLOR : SPRITE::FROZEN,
					15,
					layer
				);
			else
				TrueColor::DrawSpriteFromCurFrame(
					xPos, yPos,
					_firstFrameTrueColor + (frameID - _firstFramePaletted) * TrueColor::NumberOfFramesPerImage,
					direction,
					layer
				);
		}
		void onDraw(jjOBJ@ obj) {
			if (obj.isActive)
				_drawBodyFrame(obj, obj.xPos, obj.yPos, obj.curFrame, obj.direction);
		}
		
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) { //mostly copied from plus52Scripting.j2as
			if (bullet !is null) {
				//recreation of HANDLING::HURTBYBULLET with HANDLING::ENEMY
				if (obj.causesRicochet) {
					if ((bullet.var[6] & 6) == 0) //not fire-based, not a laser beam
						bullet.ricochet();
					else if ((bullet.var[6] & 4) == 0) //not a laser beam
						bullet.delete();
				} else if ((bullet.var[6] & 16) == 0) //not a fireball
					bullet.state = STATE::EXPLODE;
				if (obj.freeze > 0 && force < 3)
					force = 3;
				obj.energy -= force;
				obj.justHit = 5; //flash white for 5 ticks--jjOBJ::justHit is automatically deincremented by the JJ2 engine, so individual behavior functions don't need to worry about doing that.
				if (obj.energy <= 0) { //killed
					obj.energy = 0;
					if (obj.freeze > 0)
						_died = _causeOfDeath::FrozenBullet;
					else if ((bullet.var[6] & 2) == 0) //not fire-based
						_died = _causeOfDeath::Bullet;
					else if ((bullet.var[6] & 4) != 0) //laser beam
						_died = _causeOfDeath::GrayShards;
					else
						_died = ((bullet.var[6] & 8) != 0) ? _causeOfDeath::BlueShards : _causeOfDeath::OrangeShards; //powered-up (blue) or not (orange)
					if (player !is null) {
						obj.grantPickup(player, (uint(bullet.curAnim) == jjAnimSets[ANIM::AMMO].firstAnim + 17) ? 5 : 10);
						givePlayerPointsForObject(player, obj);
					}
					jjKillObject(obj.objectID);
				} else
					obj.freeze = 0;
			} else { //recreation of HANDLING::ENEMY; player guaranteed to be non-null
				if (force != 0) { //attacking via special attack, e.g. buttstomp
					obj.energy -= 4; //constant amount of damage for special attacks
					if (obj.energy <= 0) { //killed
						obj.energy = 0;
						if (obj.freeze > 0)
							_died = _causeOfDeath::FrozenPhysicalAttack;
						else
							_died = _causeOfDeath::PhysicalAttack;
						givePlayerPointsForObject(player, obj);
						jjKillObject(obj.objectID);
					} else { //only wounded
						obj.justHit = 5;
						if (obj.freeze <= 0)
							jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::COMMON_SPLAT1 + (jjRandom() & 3)));
					}
					
					if (force > 0) { //buttstomp or sugar rush
						player.buttstomp = 50; //landing
						player.ySpeed = player.ySpeed / -2 - 8;
						player.yAcc = 0;
						player.extendInvincibility(-70);
					} else if (force == -101) { //running into frozen enemy
						player.xAcc = 0;
						player.xSpeed /= -2;
						player.ySpeed = -6;
						player.extendInvincibility(-10);
					}
				} else  { //not attacking
					if (obj.freeze == 0)
						player.hurt();
				}
			}
			return true;
		}
		
		private bool givePlayerPointsForObject(jjPLAYER@ player, jjOBJ@ obj) const { //This will probably be made a jjOBJ method as part of the real JJ2+ API eventually, because it shows up all the time in the native code, but that hasn't happened yet, so here you go. Increases the player's jjPLAYER::score to match the object's jjOBJ::points, and creates a string particle with that number which flies up to the top left corner of the screen.
			if (player is null)
				return false;
			if (obj.points != 0 && (jjGameMode == GAME::SP || jjGameMode == GAME::COOP)) {
				if (_pointsFormula == PointsFormula::MultiplyByFive)
					obj.points *= 5;
				else if (_pointsFormula == PointsFormula::RoundUpToMultipleOfFifty)
					obj.points = (obj.points + 49) / 50 * 50;
				player.score += obj.points;
				jjPARTICLE@ particle = jjAddParticle(PARTICLE::STRING);
				if (particle !is null) {
					particle.xPos = obj.xPos;
					particle.yPos = obj.yPos;
					particle.xSpeed = (-32768 - int(jjRandom() & 0x3FFF)) / 65536.f;
					particle.ySpeed = (-65536 - int(jjRandom() & 0x7FFF)) / 65536.f;
					particle.string.text = formatInt(obj.points);
				}
				obj.points = 0;
				return true;
			}
			return false;
		}
		private void _fireBullets(const jjOBJ@ obj) const {
			for (uint i = 0; i < _bullets.length; ++i)
				if (-obj.var[_objVar::DirectionCurrent] != _bullets[i].direction) { //compatible with the object's current direction
					jjOBJ@ bullet = _bullets[i].fire(obj, _explosionSound, _scaleBulletSpeed ? _scale : 1.f, _scale);
					bullet.curAnim = (!_useTrueColor ? 0 : (_firstFrameTrueColor + (bullet.curFrame - _firstFramePaletted) * TrueColor::NumberOfFramesPerImage));
				}
			if (_fireSound >= 0)
				jjSample(obj.xPos, obj.yPos, SOUND::Sample(_fireSound));
		}
		
		
		int _getPresetRelativeAnimID(int relativeAnimID) const {
			if (relativeAnimID < 0) //negative numbers mean generic Jazz1 animations
				return _getAnimSet(Enemies::_Misc_Anims, _scale).animID - 1 - relativeAnimID;
			else if (relativeAnimID < 20) //small positive numbers mean animations specific to this Jazz1 enemy
				return preset.curAnim + relativeAnimID;
			else //large positive numbers are common JJ2 animations
				return relativeAnimID;
		}
		
		protected bool
			_supportsSpeed = false,
			_supportsDirection = false,
			_supportsFire = false,
			_flipSpriteWhenMovingLeft = false,
			_scaleBulletSpeed = false;
		protected float _scaledObjectSpeed = 1.f;
		
		
		protected int _adjustObjectDirection(jjOBJ@ obj) {
			switch (_direction) {
				case Directions::Right:
					obj.var[_objVar::DirectionCurrent] = 1;
					break;
				case Directions::Left:
					obj.var[_objVar::DirectionCurrent] = -1;
					break;
				case Directions::FaceJazz:
					obj.var[_objVar::DirectionCurrent] = (obj.xPos > jjLocalPlayers[0].xPos) ? -1 : 1;
					break;
				case Directions::Random:
					obj.var[_objVar::DirectionCurrent] = int(jjRandom() & 1) * 2 - 1;
					break;
				default:
					obj.var[_objVar::DirectionCurrent] = _getParameter(obj, _direction, 1) * 2 - 1;
					break;
			}
			return obj.var[_objVar::DirectionCurrent];
		}
			
		protected int _direction = Directions::Right;
		Enemy@ SetDirection(int dir) {
			if (_supportsDirection)
				_direction = dir;
			else
				jjDebug(_behaviorName + " does not support the SetDirection method.");
			return this;
		}
		protected void _reverseDirection(jjOBJ@ obj) {
			obj.var[_objVar::DirectionCurrent] = -obj.var[_objVar::DirectionCurrent];
		}
		
		protected float _speed;
		private int _speedParamOffset = -1;
		private uint _speedParamLength;
		Enemy@ SetSpeed(float speed) {
			if (_supportsSpeed) {
				_speed = abs(speed);
				_speedParamOffset = -1;
			} else
				jjDebug(_behaviorName + " does not support the SetSpeed* methods.");
			return this;
		}
		Enemy@ SetSpeedBasedOnParameter(uint o, uint l, float m = 1.0f) {
			if (_supportsSpeed) {
				_speed = abs(m);
				_speedParamOffset = o;
				_speedParamLength = l;
			} else
				jjDebug(_behaviorName + " does not support the SetSpeed* methods.");
			return this;
		}
		
		protected uint _fireDelay = 0;
		protected array<_bulletPreferences> _bullets(0);
		Enemy@ SetFireDelay(uint delay) {
			if (_supportsFire)
				_fireDelay = delay;
			else
				jjDebug(_behaviorName + " does not support the SetFireDelay method.");
			return this;
		}
		
		Enemy@ SetEnemySpeedsAreScalable(bool setTo) {
			if (_supportsSpeed)
				_scaledObjectSpeed = setTo ? _scale : 1.f;
			else
				jjDebug(_behaviorName + " does not support the SetEnemySpeedsAreScalable method.");
			return this;
		}
		Enemy@ SetBulletSpeedsAreScalable(bool setTo) {
			if (_supportsFire)
				_scaleBulletSpeed = setTo;
			else
				jjDebug(_behaviorName + " does not support the SetBulletSpeedsAreScalable method.");
			return this;
		}
		
		protected int _deathSound = -1, _fireSound = -1, _explosionSound = -1;
		Enemy@ SetDeathSound(SOUND::Sample sample) {
			_deathSound = sample;
			return this;
		}
		Enemy@ SetBulletFireSound(SOUND::Sample sample) {
			if (_supportsFire)
				_fireSound = sample;
			else
				jjDebug(_behaviorName + " does not support the SetBulletFireSound method.");
			return this;
		}
		Enemy@ SetBulletExplosionSound(SOUND::Sample sample) {
			if (_supportsFire)
				_explosionSound = sample;
			else
				jjDebug(_behaviorName + " does not support the SetBulletExplosionSound method.");
			return this;
		}
		
		private _causeOfDeath _died = _causeOfDeath::Bullet;
		protected bool _killAnimExplosion = false;
		protected uint _killAnimRepetitionCounts = 1;
		protected bool _useJJ2DeathAnimation = false;
		Enemy@ SetUsesJJ2StyleDeathAnimation(bool setTo) {
			_useJJ2DeathAnimation = setTo;
			return this;
		}
		
		Enemy@ SetWalkingEnemyCliffReaction(CliffReaction) { //overridden by Walker
			jjDebug(_behaviorName + " is not a walking enemy.");
			return this;
		}
		
		private PointsFormula _pointsFormula;
		Enemy@ SetPointsFormula(PointsFormula pointsFormula) {
			_pointsFormula = pointsFormula;
			return this;
		}
		
		uint GetFirstFrame(bool useTrueColor = false) const {
			return !useTrueColor ? _firstFramePaletted : _firstFrameTrueColor;
		}
	}
	
	Enemy@ MakeEnemy(uint8 eventID, Enemies enemyID, bool useTrueColor = false, float scale = 1.f, uint resizeMethod = Resize::Method::NearestNeighbor) {
		if (enemyID <= Enemies::_Misc_Anims || enemyID >= Enemies::LAST) {
			jjDebug("Invalid Jazz1::Enemies enum value " + enemyID + ".");
			return null;
		}
		
		jjOBJ@ preset = jjObjectPresets[eventID];
		EnemyArguments arg(preset, enemyID, useTrueColor, scale, resizeMethod);
		
		switch (enemyID) {
			case Enemies::Diamondus_TurtleGoon:				Diamondus_TurtleGoon			(arg);	break;
			case Enemies::Diamondus_BumblingBee:			Diamondus_BumblingBee			(arg);	break;
			case Enemies::Tubelectric_BlasterHorizontal:	Tubelectric_BlasterHorizontal	(arg);	break;
			case Enemies::Tubelectric_BlasterVertical:		Tubelectric_BlasterVertical		(arg);	break;
			case Enemies::Tubelectric_Spark:				Tubelectric_Spark				(arg);	break;
			case Enemies::Tubelectric_SparkBarrier:			Tubelectric_SparkBarrier		(arg);	break;
			case Enemies::Medivo_GhostRapierHorizontal:		Medivo_GhostRapierHorizontal	(arg);	break;
			case Enemies::Medivo_GhostRapierVertical:		Medivo_GhostRapierVertical		(arg);	break;
			case Enemies::Medivo_Helmut:					Medivo_Helmut					(arg);	break;
			case Enemies::Letni_Bug:						Letni_Bug						(arg);	break;
			case Enemies::Letni_BugCeiling:					Letni_BugCeiling				(arg);	break;
			case Enemies::Letni_ElecBarrier:				Letni_ElecBarrier				(arg);	break;
			case Enemies::Technoir_MiniMine:				Technoir_MiniMine				(arg);	break;
			case Enemies::Technoir_Misfire:					Technoir_Misfire				(arg);	break;
			case Enemies::Technoir_TanketyTankTank:			Technoir_TanketyTankTank		(arg);	break;
			case Enemies::Orbitus_BeholderPurple:			Orbitus_BeholderPurple			(arg);	break;
			case Enemies::Orbitus_BeholderSilver:			Orbitus_BeholderSilver			(arg);	break;
			case Enemies::Orbitus_SilverSnake:				Orbitus_SilverSnake				(arg);	break;
			case Enemies::Fanolint_FlyFlower:				Fanolint_FlyFlower				(arg);	break;
			case Enemies::Fanolint_PottedPlant:				Fanolint_PottedPlant			(arg);	break;
			case Enemies::Fanolint_SuperTankety:			Fanolint_SuperTankety			(arg);	break;
			case Enemies::Scraparap_GunnerDrone:			Scraparap_GunnerDrone			(arg);	break;
			case Enemies::Scraparap_LaunchCart:				Scraparap_LaunchCart			(arg);	break;
			case Enemies::Scraparap_RoboTurtleDrone:		Scraparap_RoboTurtleDrone		(arg);	break;
			case Enemies::Megairbase_Doofusguard:			Megairbase_Doofusguard			(arg);	break;
			case Enemies::Megairbase_Missile:				Megairbase_Missile				(arg);	break;
			case Enemies::Megairbase_SuperSpark:			Megairbase_SuperSpark			(arg);	break;
			case Enemies::Turtemple_JeTurtle:				Turtemple_JeTurtle				(arg);	break;
			case Enemies::Turtemple_ScorpWeenie:			Turtemple_ScorpWeenie			(arg);	break;
			case Enemies::Nippius_SkatePen:					Nippius_SkatePen				(arg);	break;
			case Enemies::Nippius_SkiTurtle:				Nippius_SkiTurtle				(arg);	break;
			case Enemies::Nippius_SnowGoon:					Nippius_SnowGoon				(arg);	break;
			case Enemies::Jungrock_JetSnake:				Jungrock_JetSnake				(arg);	break;
			case Enemies::Jungrock_RedBuzzer:				Jungrock_RedBuzzer				(arg);	break;
			case Enemies::Jungrock_YellowBuzzer:			Jungrock_YellowBuzzer			(arg);	break;
			case Enemies::Marbelara_Drageen:				Marbelara_Drageen				(arg);	break;
			case Enemies::Marbelara_Firebomb:				Marbelara_Firebomb				(arg);	break;
			case Enemies::Marbelara_Schwarzenguard:			Marbelara_Schwarzenguard		(arg);	break;
			case Enemies::Sluggion_Dragoon:					Sluggion_Dragoon				(arg);	break;
			case Enemies::Sluggion_RedBat:					Sluggion_RedBat					(arg);	break;
			case Enemies::Sluggion_Sluggi:					Sluggion_Sluggi					(arg);	break;
			case Enemies::Dreempipes_Minite:				Dreempipes_Minite				(arg);	break;
			case Enemies::Dreempipes_Overgrown:				Dreempipes_Overgrown			(arg);	break;
			case Enemies::Dreempipes_TerrapinSwimmer:		Dreempipes_TerrapinSwimmer		(arg);	break;
			case Enemies::Pezrox_ClammyHorizontal:			Pezrox_ClammyHorizontal			(arg);	break;
			case Enemies::Pezrox_ClammyVertical:			Pezrox_ClammyVertical			(arg);	break;
			case Enemies::Pezrox_GreenSnake:				Pezrox_GreenSnake				(arg);	break;
			case Enemies::Crysilis_GoldenBounceSpike:		Crysilis_GoldenBounceSpike		(arg);	break;
			case Enemies::Crysilis_LooGuard:				Crysilis_LooGuard				(arg);	break;
			case Enemies::Battleships_ArmorDoofi:			Battleships_ArmorDoofi			(arg);	break;
			case Enemies::Battleships_BounceSpike:			Battleships_BounceSpike			(arg);	break;
			case Enemies::Battleships_Generator:			Battleships_Generator			(arg);	break;
			case Enemies::Battleships_SuperBee:				Battleships_SuperBee			(arg);	break;
			case Enemies::Holidaius_BlueDog:				Holidaius_BlueDog				(arg);	break;
			case Enemies::Holidaius_Devil:					Holidaius_Devil					(arg);	break;
			case Enemies::Holidaius_HandHorizontal:			Holidaius_HandHorizontal		(arg);	break;
			case Enemies::Holidaius_HandVertical:			Holidaius_HandVertical			(arg);	break;
			case Enemies::Holidaius_SkiTurtle:				Holidaius_SkiTurtle				(arg);	break;
			case Enemies::Holidaius_SnowMonkey:				Holidaius_SnowMonkey			(arg);	break;
			default: return null;
		}
		
		return cast<Enemy>(cast<jjBEHAVIORINTERFACE>(preset.behavior));
	}
	
	
	/*
	import glob, os
	from PIL import Image
	os.chdir("D:\\Games\Jazz2")
	for file in glob.glob("Jazz1Enemies*.bmp"):
		Image.open(file).save(file[:-4] + ".png")
	*/
	
	
	abstract class Walker : Enemy {
		protected CliffReaction _cliffReaction = CliffReaction::TurnAround;
		Walker(const EnemyArguments &in arg) {
			super(arg);
			_flipSpriteWhenMovingLeft = _supportsSpeed = _supportsDirection = true;
		}
		void myBehave(jjOBJ@ obj) const override {
			if (obj.state == STATE::START) {
				_adjustObjectDirection(obj);
				obj.state = STATE::WALK;
				obj.putOnGround(true);
				obj.special = jjMaskedTopVLine(int(obj.xPos), int(obj.yPos), 64); //properDistanceFromGround
			}
			
			const int direction = obj.var[_objVar::DirectionCurrent];
			const int spriteWidth = jjAnimFrames[obj.curFrame].width / 2;
			const float positionForwards = obj.xPos + direction * spriteWidth;
			const float positionBackwards = obj.xPos - direction * spriteWidth;
			int distanceFromGroundForwards = jjMaskedTopVLine(int(positionForwards), int(obj.yPos), 64);
			int distanceFromGroundBackwards = jjMaskedTopVLine(int(positionBackwards), int(obj.yPos), 64);
			
			if (jjEventGet(int(positionForwards) / 32, int(obj.yPos) / 32) == AREA::STOPENEMY) {
				_reverseDirection(obj); //it takes a tick to turn around
			} else {
				if (distanceFromGroundForwards > obj.special && distanceFromGroundBackwards > obj.special) { //in the air
					obj.yPos += obj.ySpeed += 0.125f;
					if (obj.ySpeed > 4.f) obj.ySpeed = 4;
					distanceFromGroundForwards = jjMaskedTopVLine(int(positionForwards), int(obj.yPos), 64);
					distanceFromGroundBackwards = jjMaskedTopVLine(int(positionBackwards), int(obj.yPos), 64);
					int currentDistanceFromGround = (distanceFromGroundForwards < distanceFromGroundBackwards) ? distanceFromGroundForwards : distanceFromGroundBackwards;
					if (currentDistanceFromGround <= obj.special) { //landed
						obj.yPos -= obj.special - currentDistanceFromGround;
						distanceFromGroundForwards = obj.special; //don't instantly turn around
						obj.ySpeed = 0;
					}
					if (_cliffReaction != CliffReaction::Careen)
						return; //no horizontal movement
				}
				
				if (distanceFromGroundForwards < obj.special) {
					_reverseDirection(obj);
				} else if (distanceFromGroundForwards > obj.special && _cliffReaction == CliffReaction::TurnAround) {
					_reverseDirection(obj);
				} else {
					obj.xPos += direction * obj.xAcc;
				}
			}
		}
		
		Enemy@ SetWalkingEnemyCliffReaction(CliffReaction setTo) override {
			_cliffReaction = setTo;
			return this;
		}
	}
	
	const array<array<int8>> _paths = {
		{28,23,28,22,27,20,26,18,25,16,25,15,24,15,24,14,23,14,23,12,22,9,22,7,22,6,22,5,22,4,22,3,23,3,23,2,24,2,24,1,24,0,25,0,25,-1,25,-2,26,-2,26,-3,26,-4,27,-4,27,-5,28,-5,28,-6,28,-7,28,-8,29,-8,29,-9,29,-10,30,-10,30,-11,30,-12,29,-13,29,-14,28,-14,28,-15,27,-16,26,-17,25,-17,25,-18,24,-18,23,-19,22,-19,22,-20,21,-20,21,-21,21,-22,21,-23,21,-24,22,-25,23,-25,24,-26,25,-26,26,-26,26,-27,27,-27,28,-27,29,-27,29,-26,30,-26,31,-26,31,-25,32,-25,33,-24,33,-23,34,-23,34,-22,34,-21,34,-20,34,-19,34,-18,33,-18,32,-17,31,-17,30,-16,29,-16,28,-15,27,-14,26,-14,26,-13,25,-13,24,-12,23,-11,22,-11,22,-10,21,-10,21,-8,20,-8,20,-7,20,-6,19,-5,19,-4,19,-3,19,-2,20,-1,21,-1,21,0,22,0,22,2,23,4,24,5,25,5,25,6,26,6,26,7,27,7,28,8,29,8,29,10,30,11,30,12,31,12,31,13,32,13,32,14,33,14,33,15,34,15,35,16,36,17,37,17,37,18,38,18,38,19,38,20,38,21,38,22,38,23,38,24,38,25,37,25,37,26,36,26,35,26,35,27,34,27,33,27,32,27,31,27,30,27,29,27,28,27,28,26,27,26,27,25,27,24,26,24,26,23,27,23}, //bee
		{20,37,20,36,20,35,19,35,19,34,19,33,18,33,18,32,18,31,18,30,19,29,19,28,19,27,19,26,19,25,19,23,19,22,19,21,19,20,19,19,19,18,19,17,19,16,19,15,19,14,19,13,19,12,19,11,19,10,20,9,20,8,20,7,20,6,19,5,19,4,19,3,19,2,19,1,18,0,18,-1,18,-2,18,-3,18,-4,17,-5,17,-6,17,-7,17,-8,17,-10,17,-12,17,-13,17,-15,17,-17,17,-18,17,-19,17,-20,17,-21,17,-22,17,-23,17,-24,18,-24,18,-25,19,-26,20,-26,21,-27,22,-27,23,-28,24,-28,25,-28,26,-27,27,-27,27,-26,28,-25,28,-24,28,-23,28,-22,28,-21,28,-20,29,-20,29,-19,29,-18,29,-17,29,-15,29,-14,29,-13,29,-12,29,-11,29,-10,29,-9,30,-9,30,-8,30,-7,30,-6,30,-4,30,-3,30,-1,30,0,30,2,30,3,30,4,30,5,29,5,29,6,29,7,28,7,28,8,28,9,28,10,28,11,28,12,28,13,28,14,27,14,27,15,27,16,27,17,27,18,27,19,27,20,27,21,27,22,27,23,28,23,28,24,28,25,29,25,29,26,29,27,28,28,28,29,28,30,27,30,27,31,26,32,25,33,24,33,24,34,24,35,24,36,23,36,23,37,23,38,22,38,21,38,20,38,20,37,19,37,19,36}, //rapier
		{22,4,22,3,22,2,22,1,22,0,22,-1,22,-2,22,-3,22,-4,22,-5,22,-6,22,-7,22,-8,22,-9,22,-10,22,-11,22,-12,22,-13,22,-14,22,-15,22,-16,22,-17,22,-18,22,-19,22,-20,22,-19,22,-18,22,-17,22,-16,22,-15,22,-14,22,-13,22,-12,22,-11,22,-10,22,-9,22,-8,22,-7,22,-6,22,-5,22,-4,22,-3,22,-2,22,-1,22,0,22,1,22,2,22,3,22,4,22,5,22,6,22,7,22,8,22,9,22,10,22,12,22,13,22,14,22,15,22,16,22,17,22,18,22,19,22,20,22,21,22,22,22,23,22,24,22,25,22,26,22,27,22,28,22,29,22,30,22,31,22,30,22,29,22,28,22,27,22,26,22,25,22,23,21,22,21,20,21,19,21,18,21,17,21,16,21,15,21,14,21,13,21,12,21,11,21,10,21,9,21,8,21,7,21,6,21,5,21,4}, //silversnake
		{9,10,9,9,9,8,9,7,9,6,9,5,9,4,9,3,9,2,9,1,9,0,9,-1,9,-2,9,-3,9,-4,9,-5,9,-6,9,-7,9,-8,9,-9,9,-10,10,-10,10,-11,11,-12,12,-12,12,-13,13,-14,14,-14,15,-15,16,-15,17,-16,18,-16,19,-16,20,-16,21,-16,22,-16,23,-16,24,-16,25,-16,26,-16,27,-16,28,-16,29,-15,30,-15,30,-14,30,-13,30,-12,30,-11,30,-10,30,-9,30,-8,30,-7,30,-6,30,-5,30,-4,30,-3,30,-2,29,-1,29,0,29,1,29,2,28,3,28,4,28,5,28,6,28,7,28,8,28,9,28,10,27,11,27,12,27,13,26,13,25,14,24,15,23,15,22,15,21,16,20,16,19,16,18,16,17,16,16,16,15,16,14,16,13,16,12,16,11,16,10,15,9,15,8,15,8,14,8,13,8,12,8,11}, //devil
		{24,-5,23,-5,23,-7,23,-8,24,-8,24,-9,24,-10,24,-11,24,-12,24,-14,24,-16,24,-19,24,-21,24,-23,23,-26,23,-28,23,-31,23,-33,23,-36,23,-39,23,-43,23,-47,23,-50,23,-53,23,-54,23,-56,23,-57,23,-58,24,-58,24,-59,24,-60,25,-62,25,-63,25,-65,25,-66,25,-67,26,-67,26,-68,27,-68,27,-69,28,-69,28,-70,29,-70,30,-70,30,-71,31,-71,32,-72,33,-72,34,-72,35,-72,36,-72,37,-72,37,-71,38,-71,38,-70,39,-70,39,-69,40,-67,40,-65,40,-62,40,-60,40,-57,40,-53,40,-49,40,-45,40,-40,40,-34,40,-28,40,-21,41,-14,41,-7,41,0,41,7,41,13,41,20,41,25,41,28,41,33,42,37,42,40,43,42,43,45,43,46,43,47,43,48,42,48,42,49,41,49,41,50,41,51,40,51,40,52,39,52,39,53,38,53,37,53,36,53,35,53,34,53,33,53,33,52,32,52,31,52,31,51,30,51,30,50,29,50,29,49,28,48,28,47,27,47,27,46,27,44,26,42,26,40,26,37,26,35,25,33,25,32,25,30,25,27,25,24,25,22,25,19,24,17,24,15,24,13,23,12,23,11,23,10,23,9,23,8,23,7,23,6,23,5,23,4,23,3,23,2,23,1,23,0,23,-1,23,-2,23,-3,23,-4}, //fanflower hard
		{58,-21,57,-20,56,-19,55,-18,54,-17,53,-16,52,-15,51,-15,50,-14,50,-13,49,-13,49,-12,49,-11,48,-11,48,-10,48,-9,48,-8,48,-7,48,-6,49,-6,50,-5,51,-5,52,-4,53,-3,54,-3,54,-2,55,-2,56,-1,57,-1,57,0,58,0,58,1,59,1,59,2,60,3,60,4,61,4,61,5,61,6,61,7,61,8,60,8,60,9,59,10,58,11,58,12,57,12,56,12,56,13,55,13,54,14,53,14,53,15,52,15,52,16,51,16,50,16,49,16,48,16,47,16,46,16,45,16,45,15,44,15,43,15,43,14,42,14,42,13,42,12,42,11,43,11,44,10,44,9,45,9,45,8,46,8,46,7,47,7,47,6,48,6,49,6,50,5,51,5,51,4,52,4,53,4,53,3,54,3,55,3,55,2,56,2,56,1,57,1,57,0,58,0,58,-1,59,-1,59,-2,60,-2,60,-3,61,-3,61,-4,62,-4,62,-5,63,-6,63,-7,64,-7,64,-8,64,-9,65,-9,65,-10,65,-11,65,-12,65,-13,66,-13,66,-14,66,-15,66,-16,66,-17,66,-18,65,-18,65,-19,64,-20,64,-21,63,-21,63,-22,62,-22,61,-22,60,-22,59,-22,59,-21,58,-21}, //fanflower easy
		{23,-2, 23,-3, 23,-4, 23,-6, 23,-8, 23,-9, 23,-10, 23,-11, 23,-12, 23,-13, 23,-14, 23,-15, 23,-16, 23,-17, 23,-18, 23,-19, 23,-20, 23,-21, 23,-22, 23,-23, 23,-24, 23,-25, 23,-24, 24,-24, 24,-23, 24,-22, 24,-21, 24,-20, 24,-19, 24,-18, 24,-17, 24,-16, 24,-14, 24,-12, 24,-11, 24,-10, 24,-9, 24,-7, 24,-6, 24,-4, 24,-3, 24,-1, 24,0, 24,1, 24,2, 24,3, 24,4, 24,5, 24,6, 24,7, 24,8, 24,9, 24,10, 24,12, 24,13, 24,14, 24,15, 24,16, 24,17, 24,18, 24,19, 24,20, 24,21, 24,22, 24,23, 24,24, 24,25, 24,26, 24,27, 24,28, 24,29, 24,30, 24,31, 24,32, 24,33, 24,34, 24,35, 24,36, 23,36, 22,36, 21,35, 20,35, 19,35, 19,34, 18,33, 17,32, 17,31, 16,30, 16,29, 16,28, 16,27, 16,26, 16,25, 16,24, 16,23, 17,22, 17,21, 18,20, 19,19, 20,17, 21,16, 22,14, 23,12, 24,12, 24,11, 24,10, 24,9, 24,8, 25,7, 25,6, 26,5, 26,4, 27,2, 28,0, 29,-2, 29,-4, 29,-6, 29,-7, 29,-8, 29,-9, 29,-10, 29,-11, 29,-12, 29,-13, 29,-15, 29,-16, 29,-17, 29,-18, 29,-19, 29,-20, 28,-22, 28,-23, 27,-26, 26,-28, 25,-29, 25,-30, 25,-31, 24,-31, 23,-31, 23,-30, 22,-30, 21,-29, 20,-29, 20,-28, 20,-27, 20,-26, 20,-25, 20,-24, 20,-23, 20,-22, 20,-21, 20,-20, 20,-19, 20,-18, 20,-17, 20,-16, 20,-15, 20,-14, 20,-13, 20,-12, 20,-11, 20,-10, 20,-9, 20,-8, 20,-7, 20,-6, 20,-5, 20,-4, 20,-3, 20,-2, 20,-1, 20,0, 20,-1, 20,-2}, //redbuz
		{18,5, 19,4, 19,2, 20,1, 20,-1, 21,-2, 21,-4, 22,-5, 22,-7, 23,-8, 23,-10, 24,-11, 24,-13, 25,-14, 25,-16, 26,-17, 26,-19, 27,-20, 27,-22, 28,-23, 28,-25, 28,-27, 30,-27, 31,-28, 32,-29, 33,-29, 35,-30, 36,-31, 37,-31, 38,-32, 40,-33, 41,-33, 42,-34, 42,-34, 44,-34, 45,-33, 47,-33, 48,-32, 48,-31, 48,-30, 49,-28, 49,-27, 50,-25, 50,-24, 49,-23, 49,-21, 49,-19, 49,-17, 50,-16, 50,-14, 50,-12, 50,-10, 50,-8, 49,-7, 48,-6, 48,-4, 48,-3, 47,-1, 47,1, 47,2, 46,4, 46,5, 46,7, 45,8, 45,10, 43,10, 43,12, 42,13, 41,14, 41,15, 40,17, 39,18, 39,19, 38,21, 38,22, 37,23, 36,24, 36,26, 35,27, 32,26, 31,27, 29,27, 27,27, 26,27, 24,28, 23,28, 21,28, 18,27, 17,26, 15,26, 13,23, 13,22, 12,20, 12,19, 12,17, 12,16, 13,14, 14,13, 15,12, 15,11, 16,10, 17,8, 17,7, 18,6}, //marbel
		{23,16, 23,15, 22,15, 22,14, 21,14, 21,13, 21,12, 20,12, 20,11, 20,10, 19,10, 19,9, 19,8, 19,7, 19,6, 19,5, 19,4, 20,4, 20,3, 21,3, 21,2, 22,2, 23,2, 23,1, 24,1, 25,0, 25,-1, 26,-1, 26,-2, 26,-3, 26,-4, 26,-5, 26,-6, 26,-7, 26,-8, 25,-8, 25,-9, 24,-9, 24,-10, 23,-10, 22,-11, 21,-12, 20,-12, 20,-13, 19,-14, 20,-14, 20,-15, 21,-15, 22,-15, 23,-15, 24,-15, 24,-16, 25,-16, 26,-16, 26,-15, 27,-15, 28,-15, 29,-15, 29,-14, 30,-14, 30,-13, 30,-12, 31,-12, 31,-11, 31,-10, 31,-9, 30,-8, 29,-8, 29,-7, 28,-7, 27,-7, 27,-6, 26,-6, 25,-6, 24,-6, 23,-5, 22,-5, 21,-5, 21,-4, 20,-4, 19,-4, 19,-3, 19,-2, 19,-1, 20,-1, 20,0, 21,0, 21,1, 22,1, 22,2, 23,2, 24,2, 25,2, 26,2, 26,3, 27,3, 28,3, 28,4, 29,4, 30,5, 31,5, 31,6, 31,7, 31,8, 30,8, 30,9, 29,9, 29,10, 28,10, 27,10, 26,11, 25,11, 24,11, 24,12, 23,12, 22,13, 21,13, 21,14, 20,14, 20,15, 20,16, 19,16, 19,17, 19,18, 19,19, 19,20, 19,21, 20,22, 21,22, 21,23, 22,23, 23,23, 24,23, 25,23, 26,23, 26,22, 27,22, 27,21, 27,20, 26,20, 26,19, 25,19, 25,18, 24,17, 24,16}, //sluggy
		{53,17, 53,16, 53,15, 53,14, 53,13, 53,12, 53,11, 53,10, 53,9, 53,8, 53,6, 54,6, 54,5, 54,4, 54,3, 54,2, 54,1, 55,0, 55,-1, 55,-2, 55,-3, 55,-4, 55,-5, 55,-6, 55,-8, 55,-10, 55,-11, 55,-12, 55,-13, 55,-14, 56,-14, 56,-15, 56,-16, 56,-17, 56,-18, 56,-19, 57,-19, 57,-20, 57,-21, 58,-21, 59,-21, 60,-22, 61,-22, 62,-22, 63,-23, 64,-23, 65,-23, 66,-23, 67,-23, 68,-23, 69,-23, 70,-23, 70,-22, 71,-22, 72,-22, 73,-22, 74,-22, 74,-21, 74,-20, 74,-19, 74,-18, 74,-17, 74,-15, 74,-14, 73,-12, 73,-10, 73,-8, 73,-6, 73,-4, 73,-2, 72,0, 72,3, 72,5, 71,8, 71,12, 71,15, 71,19, 71,23, 70,26, 70,30, 70,32, 70,33, 70,34, 69,35, 69,36, 69,37, 68,37, 67,37, 66,37, 65,37, 64,37, 63,37, 62,37, 62,36, 61,36, 60,36, 59,36, 58,36, 57,36, 56,36, 55,36, 54,35, 53,35, 52,35, 52,34, 52,33, 52,31, 52,30, 53,29, 53,28, 53,27, 53,26, 53,25, 53,24, 53,23, 53,22, 53,21, 53,20, 53,19, 53,18, 53,17}, //batred
		{12,21, 12,20, 13,19, 13,18, 13,17, 13,16, 13,15, 13,14, 13,13, 13,12, 14,11, 14,10, 15,10, 15,9, 16,9, 16,8, 17,8, 17,7, 17,6, 18,5, 18,4, 19,4, 19,3, 20,2, 20,1, 20,0, 21,-1, 21,-2, 22,-3, 22,-4, 22,-5, 22,-6, 23,-8, 23,-9, 23,-10, 23,-11, 23,-12, 23,-13, 23,-14, 23,-15, 23,-16, 23,-17, 23,-18, 23,-19, 23,-20, 22,-20, 21,-20, 20,-20, 19,-19, 18,-18, 17,-17, 16,-17, 15,-16, 14,-15, 14,-14, 13,-13, 13,-12, 13,-11, 13,-10, 13,-9, 13,-8, 13,-7, 13,-6, 13,-5, 13,-4, 13,-3, 13,-2, 13,-1, 13,0, 14,1, 14,2, 15,3, 16,4, 17,5, 18,5, 19,6, 20,7, 21,8, 22,9, 23,11, 24,12, 25,13, 26,14, 26,15, 26,16, 26,17, 26,19, 26,20, 26,21, 26,23, 26,24, 26,25, 26,27, 26,28, 26,31, 26,32, 26,34, 26,35, 26,37, 25,38, 25,39, 25,40, 25,41, 24,42, 24,43, 23,44, 22,44, 21,45, 21,46, 20,46, 20,47, 19,47, 18,48, 17,48, 16,48, 15,49, 14,49, 13,49, 12,49, 11,49, 11,48, 10,48, 10,47, 10,46, 10,45, 10,44, 10,43, 10,42, 11,41, 11,40, 12,39, 13,39, 13,38, 13,37, 13,36, 14,36, 14,35, 14,34, 14,33, 14,32, 14,31, 14,30, 14,29, 14,28, 14,27, 14,26, 14,25, 14,24, 14,23, 14,22}, //loogies
		{20,18, 19,17, 18,16, 18,15, 17,15, 17,14, 16,14, 16,13, 15,12, 15,11, 15,10, 15,9, 15,7, 15,5, 15,2, 15,0, 16,-2, 16,-4, 17,-4, 18,-5, 19,-5, 19,-7, 20,-7, 20,-8, 21,-8, 21,-9, 22,-9, 22,-10, 23,-10, 23,-11, 23,-12, 24,-12, 24,-13, 24,-14, 24,-15, 24,-16, 24,-17, 24,-19, 24,-21, 24,-23, 24,-24, 24,-25, 24,-26, 23,-26, 22,-27, 21,-27, 21,-28, 20,-28, 19,-28, 19,-29, 18,-29, 17,-29, 16,-29, 15,-29, 14,-29, 13,-29, 12,-28, 11,-28, 10,-28, 10,-27, 9,-27, 9,-26, 8,-26, 8,-25, 7,-25, 7,-24, 7,-23, 7,-22, 7,-21, 7,-20, 8,-20, 8,-19, 8,-18, 9,-18, 10,-17, 10,-16, 11,-16, 11,-15, 12,-14, 13,-14, 13,-13, 14,-13, 15,-12, 16,-12, 16,-11, 17,-11, 18,-11, 18,-10, 19,-10, 20,-9, 21,-8, 22,-8, 22,-7, 23,-7, 23,-5, 24,-4, 24,-3, 25,-3, 25,-2, 26,-2, 27,-1, 27,1, 28,2, 28,5, 29,7, 29,9, 30,11, 30,12, 31,12, 31,13, 31,14, 30,14, 30,15, 30,16, 29,16, 28,17, 27,17, 26,17, 26,18, 25,18, 24,18, 23,18, 22,18, 21,18} //battlebee
	};
	abstract class PathFollower : Enemy {
		protected const array<int8>@ _path;
		PathFollower(const EnemyArguments &in arg, int pathID) {
			super(arg);
			@_path = _paths[pathID];
			preset.yAcc = 0; //distance moved
			_flipSpriteWhenMovingLeft = _supportsSpeed = true;
		}
		void myBehave(jjOBJ@ obj) const override {
			if (obj.state == STATE::START) {
				_adjustObjectDirection(obj);
				obj.state = STATE::FLY;
				obj.xAcc /= 4 * _scaledObjectSpeed; //adjust for not moving in blocks of 4 pixels at a time anymore
			}
			obj.yAcc += obj.xAcc; //move forwards
			const int numberOfPointsOnPath = _path.length / 2;
			const int point1 = (int(obj.yAcc) % numberOfPointsOnPath) * 2;
			const float nearnessToNextPoint = obj.yAcc % 1.f; //instead of jumping from point to point, move smoothly during the transition gameticks
			
			const float directionMovingAhead = _path[(point1+3) % _path.length] - _path[point1+1];
			if (directionMovingAhead > 0)
				obj.var[_objVar::DirectionCurrent] = 1;
			else if (directionMovingAhead < 0)
				obj.var[_objVar::DirectionCurrent] = -1;
			
			obj.xPos = obj.xOrg + (_path[point1+1] + nearnessToNextPoint * directionMovingAhead) * 4 * _scaledObjectSpeed;
			obj.yPos = obj.yOrg + (_path[point1+0] + nearnessToNextPoint * (_path[(point1+2) % _path.length] - _path[point1+0])) * _scaledObjectSpeed;
		}
	}
	abstract class Snake : PathFollower {
		protected uint
			_segmentStrength = 1, //"strength" in J1E--jjOBJ::energy should be a multiple of this and "moving platform start"
			_segmentSeparation = 1, //"var 2" in J1E
			_segmentDropAnimationLength = 19;
		Snake(const EnemyArguments &in arg, int pathID) {
			super(arg, pathID);
			if (jjDifficulty >= 3)
				_segmentStrength += 1;
		}
		void myBehave(jjOBJ@ obj) const override {
			if (jjDifficulty >= 3 && obj.state == STATE::START) //turbo
				obj.energy = (obj.energy - 1) / (_segmentStrength - 1) * _segmentStrength; //undo normal turbo strength boost
			PathFollower::myBehave(obj);
		}
		void onDraw(jjOBJ@ obj) override {
			const uint numberOfSegments = (obj.energy + (_segmentStrength-1)) / _segmentStrength;
			const uint numberOfPointsOnPath = _path.length >> 1;
			
			float oldPosition = obj.yAcc - obj.xAcc * _segmentSeparation * numberOfSegments;
			uint i = 0;
			while (true) {
				const float nearnessToNextPoint = (oldPosition) % 1.f; //instead of jumping from point to point, move smoothly during the transition gameticks
				int pointIndex = int(oldPosition);
				while (pointIndex < 0) pointIndex += numberOfPointsOnPath; //% doesn't really work with negative numbers
				pointIndex %= numberOfPointsOnPath;
				const uint pointIndex2 = ((pointIndex + 1) % numberOfPointsOnPath) << 1;
				pointIndex <<= 1;
				_drawBodyFrame(
					obj,
					obj.xOrg + (_path[pointIndex + 1] + nearnessToNextPoint * (_path[pointIndex2 + 1] - _path[pointIndex + 1])) * 4 * _scaledObjectSpeed,
					obj.yOrg + (_path[pointIndex + 0] + nearnessToNextPoint * (_path[pointIndex2 + 0] - _path[pointIndex + 0])) * _scaledObjectSpeed,
					jjAnimations[obj.curAnim + 1] + (i == 0 ? 1 : 0), //hardcoded: snake body, tail are the two frames immediately following the head's animation
					1
				);
				if (++i >= numberOfSegments)
					break;
				oldPosition += obj.xAcc * _segmentSeparation;
			}
			
			Enemy::onDraw(obj);
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) override {
			const auto numberOfSegmentsOld = (obj.energy + (_segmentStrength-1)) / _segmentStrength;
			Enemy::onObjectHit(obj, bullet, player, force);
			if (obj.isActive && ((obj.energy + (_segmentStrength-1)) / _segmentStrength) < numberOfSegmentsOld) { //hurt; drop a segment
				jjOBJ@ shard = jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos, _segmentDropAnimationLength)];
				shard.curAnim = obj.curAnim + 1;
				shard.behavior = _explosion;
				shard.counterEnd = 0;
				shard.xSpeed = ((bullet !is null) ? bullet.xSpeed : player.xSpeed) / 2;
				shard.ySpeed = ((bullet !is null) ? bullet.ySpeed : player.ySpeed) / 2 + 0.0001; //non-zero
			}
			return true;
		}
	}
	
	abstract class StalkerGhost : Enemy {
		StalkerGhost(const EnemyArguments &in arg) {
			super(arg);
			_flipSpriteWhenMovingLeft = _supportsSpeed = _supportsDirection = true;
		}
		void myBehave(jjOBJ@ obj) const override {
			if (obj.state == STATE::START) {
				_adjustObjectDirection(obj); //get an initial direction in case the player is facing us
				obj.state = STATE::FLY;
			}
			const int nearestPlayerID = obj.findNearestPlayer(320*320);
			if (nearestPlayerID >= 0) {
				const jjPLAYER@ play = jjPlayers[nearestPlayerID];
				const bool objectToRightOfPlayer = obj.xPos > play.xPos;
				if ((play.direction >= 0) != objectToRightOfPlayer && abs(obj.xPos - play.xPos) > 20) { //player looking away from a sufficiently distant enemy
					obj.var[_objVar::DirectionCurrent] = objectToRightOfPlayer ? -1 : 1;
					obj.xPos += obj.xAcc * obj.var[_objVar::DirectionCurrent];
					const float yDist = obj.yPos - play.yPos;
					if (abs(yDist) > 20) { //move vertically... but only do so if moving horizontally
						if (yDist > 0)
							obj.yPos -= obj.xAcc / 4;
						else
							obj.yPos += obj.xAcc / 4;
					}
				}
			}
		}
	}
	
	abstract class Missile : Enemy {
		Missile(const EnemyArguments &in arg) {
			super(arg);
			_flipSpriteWhenMovingLeft = _supportsSpeed = _supportsDirection = true;
		}
		void myBehave(jjOBJ@ obj) const override {
			if (obj.state == STATE::START) {
				_adjustObjectDirection(obj); //get an initial direction in case the player is facing us
				obj.state = STATE::FLY;
			}
			obj.xPos += obj.xAcc * obj.var[_objVar::DirectionCurrent];
		}
	}
	
	abstract class Floater : Enemy {
		Floater(const EnemyArguments &in arg) {
			super(arg);
			_flipSpriteWhenMovingLeft = _supportsSpeed = _supportsDirection = true;
		}
		void myBehave(jjOBJ@ obj) const override {
			if (obj.state == STATE::START) {
				_adjustObjectDirection(obj);
				obj.state = STATE::FLY;
			}
			
			const int direction = obj.var[_objVar::DirectionCurrent];
			const jjANIMFRAME@ animFrame = jjAnimFrames[obj.curFrame];
			const float positionForwards = obj.xPos + direction * animFrame.width / 2;
			
			if (
				(jjEventGet(int(positionForwards) / 32, int(obj.yPos) / 32) == AREA::STOPENEMY) ||
				(jjMaskedVLine(int(positionForwards), int(obj.yPos + animFrame.hotSpotY), animFrame.height)) //wall
			)
				_reverseDirection(obj);
			else
				obj.xPos += direction * obj.xAcc;
		}
	}
	
	abstract class WallStickerHorizontal : Enemy {
		WallStickerHorizontal(const EnemyArguments &in arg) {
			super(arg);
			_flipSpriteWhenMovingLeft = _supportsDirection = true;
		}
		void myBehave(jjOBJ@ obj) const override {
			if (obj.state == STATE::START) {
				if (_adjustObjectDirection(obj) == 1) {
					while (obj.xPos > 0 && !_maskedPixelFloat(obj.xPos, obj.yPos))
						obj.xPos -= 1;
				} else {
					obj.direction = SPRITE::FLIPH;
					while (obj.xPos < _levelWidth-2 && !_maskedPixelFloat(obj.xPos, obj.yPos))
						obj.xPos += 1;
				}
				obj.state = STATE::WAIT;
			}
		}
	}
	abstract class WallStickerVertical : Enemy {
		WallStickerVertical(const EnemyArguments &in arg) {
			super(arg);
			_supportsDirection = true;
		}
		void myBehave(jjOBJ@ obj) const override {
			if (obj.state == STATE::START) {
				if (_adjustObjectDirection(obj) == 1) { //right = floor
					while (obj.yPos < _levelHeight-2 && !_maskedPixelFloat(obj.xPos, obj.yPos))
						obj.yPos += 1;
				} else { //left = ceiling
					obj.direction = SPRITE::FLIPV;
					while (obj.yPos > 0 && !_maskedPixelFloat(obj.xPos, obj.yPos))
						obj.yPos -= 1;
				}
				obj.state = STATE::WAIT;
			}
		}
	}
	
	abstract class StandOnFloor : Enemy {
		StandOnFloor(const EnemyArguments &in arg) {
			super(arg);
		}
		void myBehave(jjOBJ@ obj) const override {
			if (obj.state == STATE::START) {
				obj.putOnGround(true);
				obj.state = STATE::WAIT;
			}
			if (_supportsDirection)
				_adjustObjectDirection(obj);
		}
	}
	
	abstract class Hopper : Enemy {
		private uint8 _timeBetweenHops;
		private float _heightOfHops, _lengthOfHops;
		Hopper(const EnemyArguments &in arg, uint8 var1, uint8 var2) {
			super(arg);
			_heightOfHops = 1;
			_lengthOfHops = 2 + (var1 << 3); //(2), 10, 18, 26, 34, 42...
			uint yTopIncrease = 3;
			while (var1-- != 0) {
				_heightOfHops += yTopIncrease; //(1), 4, 11, 22, 37, 56, 79, 106, 137, 172, 211...
				yTopIncrease += 4;
			}
			_timeBetweenHops = var2;
			if (_animFrames.length & 1 == 1) //odd number
				_animFrames.insertLast(_animFrames[_animFrames.length-1]);
		}
		void myBehave(jjOBJ@ obj) const override {
			bool facingDown = true;
			if (obj.state == STATE::START) {
				obj.yPos = obj.yOrg -= 1;
				obj.state = STATE::WAIT;
			} else if (obj.state != STATE::BOUNCE) {
				if (obj.counterEnd++ >= _timeBetweenHops) {
					obj.counterEnd = 0;
					obj.state = STATE::BOUNCE;
				}
			} else {
				if (obj.counterEnd++ >= uint(_lengthOfHops)) {
					obj.counterEnd = 0;
					obj.yPos = obj.yOrg;
					if (_timeBetweenHops != 0) obj.state = STATE::WAIT;
				} else {
					obj.yPos = obj.yOrg - jjSin(int((uint(obj.counterEnd) << 9) / _lengthOfHops)) * _heightOfHops * _scaledObjectSpeed;
					facingDown = obj.counterEnd > uint(_lengthOfHops) / 2;
				}
			}
			
			obj.var[_objVar::AnimCounter] = ((facingDown ? _animFrames.length : 0) + ((jjGameTicks / (obj.animSpeed + 1) % _animFrames.length))) - 1;
		}
		Enemy@ SetFireDelay(uint delay) override {
			_timeBetweenHops = delay;
			return this;
		}
		Enemy@ SetEnemySpeedsAreScalable(bool setTo) override {
			_scaledObjectSpeed = setTo ? _scale : 1.f;
			return this;
		}
	}
	
	
	
	final class Diamondus_TurtleGoon : Walker {
		Diamondus_TurtleGoon(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.333333;
			_direction = Directions::Right;
			_killAnimExplosion = true;
			preset.killAnim = -1;
			preset.points = 100;
			preset.energy = 1;
			preset.animSpeed = 4;
		}
	}
	
	final class Diamondus_BumblingBee : PathFollower {
		Diamondus_BumblingBee(const EnemyArguments &in arg) {
			super(arg, 0);
			_speed = 2;
			_killAnimExplosion = true;
			preset.killAnim = -1;
			preset.points = 50;
			preset.energy = 1;
			preset.animSpeed = 3;
		}
	}
	
	
	final class Tubelectric_BlasterHorizontal : WallStickerHorizontal {
		Tubelectric_BlasterHorizontal(const EnemyArguments &in arg) {
			super(arg);
			_supportsFire = true;
			@_animFrames = array<int8> = {0, 1, 2, 1};
			@_fireFrames = array<int8> = {2,2,2,2,2};
			_bullets.insertLast(_bulletPreferences(-3,0, 1,0, _bulletDirection::Left));
			_bullets.insertLast(_bulletPreferences( 3,0, 1,0, _bulletDirection::Right));
			_fireDelay = 75;
			preset.playerHandling = HANDLING::SELFCOLLISION;
			preset.animSpeed = 4;
		}
	}
	final class Tubelectric_BlasterVertical : WallStickerVertical {
		Tubelectric_BlasterVertical(const EnemyArguments &in arg) {
			super(arg);
			_supportsFire = true;
			@_animFrames = array<int8> = {0, 1, 2, 1};
			@_fireFrames = array<int8> = {2,2,2,2,2};
			_bullets.insertLast(_bulletPreferences(0, 3, 1,0, _bulletDirection::Left));
			_bullets.insertLast(_bulletPreferences(0,-3, 1,0, _bulletDirection::Right));
			_fireDelay = 75;
			preset.playerHandling = HANDLING::SELFCOLLISION;
			preset.animSpeed = 4;
		}
	}
	
	final class Tubelectric_Spark : StalkerGhost {
		Tubelectric_Spark(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.33333333;
			_direction = Directions::Left;
			@_animFrames = array<int8> = {0, 0, 0, 1}; //spend thrice as much time not-flashing as flashing
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 6;
			preset.killAnim = 1;
			preset.points = 20;
			preset.energy = 1;
			preset.animSpeed = 0;
		}
	}
	
	final class Tubelectric_SparkBarrier : Enemy {
		Tubelectric_SparkBarrier(const EnemyArguments &in arg) {
			super(arg);
			@_animFrames = array<int8> = {0, 1, 2, 1};
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 7;
			preset.killAnim = 1;
			preset.points = 20;
			preset.energy = 3;
			preset.animSpeed = 2;
		}
	}
	
	
	final class Medivo_GhostRapierHorizontal : Missile {
		Medivo_GhostRapierHorizontal(const EnemyArguments &in arg) {
			super(arg);
			_speed = 2;
			_direction = Directions::FaceJazz;
			preset.points = 10;
			preset.energy = 1;
			preset.animSpeed = 1;
		}
	}
	
	final class Medivo_GhostRapierVertical : PathFollower {
		Medivo_GhostRapierVertical(const EnemyArguments &in arg) {
			super(arg, 1);
			_speed = 1.333333;
			_flipSpriteWhenMovingLeft = false;
			preset.points = 10;
			preset.energy = 1;
			preset.animSpeed = 1;
		}
	}
	
	final class Medivo_Helmut : Walker {
		Medivo_Helmut(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.333333;
			_cliffReaction = CliffReaction::Fall;
			preset.points = 10;
			preset.energy = 1;
			preset.animSpeed = 3;
			_direction = Directions::Left;
		}
	}
	
	
	final class Letni_Bug : Walker {
		Letni_Bug(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.333333;
			preset.points = 50;
			preset.energy = 1;
			preset.animSpeed = 3;
			_direction = Directions::Left;
		}
	}
	final class Letni_BugCeiling : Enemy {
		Letni_BugCeiling(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.333333;
			preset.points = 50;
			preset.energy = 1;
			preset.animSpeed = 3;
			_flipSpriteWhenMovingLeft = _supportsSpeed = _supportsDirection = true;
			_direction = Directions::Left;
			_killAnimExplosion = true; //different from floor variation for some reason
		}
		void myBehave(jjOBJ@ obj) const override { //like Walker, but on ceiling and without any falling options
			if (obj.state == STATE::START) {
				{
					_adjustObjectDirection(obj);
					while (obj.yPos >= 5 && !_maskedPixelFloat(obj.xPos, obj.yPos - 5))
						obj.yPos -= 1;
				}
				obj.state = STATE::WALK;
			}
			
			const int direction = obj.var[_objVar::DirectionCurrent];
			const float positionForwards = obj.xPos + direction * jjAnimFrames[obj.curFrame].width / 2;
			
			if (
				(jjEventGet(int(positionForwards) / 32, int(obj.yPos) / 32) == AREA::STOPENEMY) ||
				(!_maskedPixelFloat(positionForwards, obj.yPos - 5)) || //cliff
				(_maskedPixelFloat(positionForwards, obj.yPos - 4)) //wall
			)
				_reverseDirection(obj);
			else
				obj.xPos += direction * obj.xAcc;
		}
	}
	
	final class Letni_ElecBarrier : Enemy {
		Letni_ElecBarrier(const EnemyArguments &in arg) {
			super(arg);
			_killAnimExplosion = true;
			preset.killAnim = -2;
			preset.points = 50;
			preset.energy = 3;
			preset.animSpeed = 3;
		}
	}
	
	
	final class Technoir_MiniMine : Floater {
		Technoir_MiniMine(const EnemyArguments &in arg) {
			super(arg);
			_speed = 0.8f;
			_flipSpriteWhenMovingLeft = false;
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 10;
			preset.killAnim = 1;
			preset.points = 0;
			preset.energy = 3;
			preset.animSpeed = 1;
		}
	}
	
	final class Technoir_Misfire : Walker { //actually an amalgamation of the Misfires from the two Technoir levels; level 1's movement speed and points, level 2's falling off cliffs
		Technoir_Misfire(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.333333;
			_cliffReaction = CliffReaction::Fall;
			preset.points = 50;
			preset.energy = 1;
			preset.animSpeed = 3;
			_direction = Directions::Left;
			_killAnimExplosion = true;
		}
	}
	
	final class Technoir_TanketyTankTank : Walker {
		Technoir_TanketyTankTank(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1;
			preset.points = 50;
			preset.energy = 2;
			preset.animSpeed = 3;
			_direction = Directions::Left;
			_killAnimExplosion = true;
			_supportsFire = true;
			@_animFrames = array<int8> = {0, 1, 2, 3};
			@_fireFrames = array<int8> = {4,4,4,4,4,4};
			_bullets.insertLast(_bulletPreferences(-4,0, 1,1, _bulletDirection::Left));
			_bullets.insertLast(_bulletPreferences( 4,0, 1,0, _bulletDirection::Right));
			_fireDelay = 100;
		}
	}
	
	
	final class Orbitus_BeholderPurple : Enemy {
		Orbitus_BeholderPurple(const EnemyArguments &in arg) {
			super(arg);
			preset.points = 30;
			preset.energy = 4;
			preset.animSpeed = 4;
			@_animFrames = array<int8> = {0, 1, 2, 3, 2, 1};
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 9;
			preset.killAnim = 1;
		}
	}
	
	final class Orbitus_BeholderSilver : Walker {
		Orbitus_BeholderSilver(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1;
			preset.points = 30;
			preset.energy = 1;
			preset.animSpeed = 5;
			@_animFrames = array<int8> = {0, 1, 2, 3, 2, 1};
			_direction = Directions::Left;
			_flipSpriteWhenMovingLeft = false;
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 9;
			preset.killAnim = 1;
		}
	}
	
	final class Orbitus_SilverSnake : Snake {
		Orbitus_SilverSnake(const EnemyArguments &in arg) {
			super(arg, 2);
			_speed = 4;
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 19;
			preset.killAnim = 1;
			preset.points = 20;
			preset.energy = 10;
			preset.animSpeed = 6;
		}
	}
	
	
	final class Fanolint_FlyFlower : PathFollower {
		Fanolint_FlyFlower(const EnemyArguments &in arg) {
			super(arg, jjDifficulty >= 2 ? 4 : 5);
			_speed = 2;
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 11;
			preset.killAnim = 1;
			preset.points = 30;
			preset.energy = 1;
			preset.animSpeed = 5;
		}
	}
	
	final class Fanolint_PottedPlant : StandOnFloor {
		Fanolint_PottedPlant(const EnemyArguments &in arg) {
			super(arg);
			preset.points = 20;
			preset.energy = 3;
			preset.animSpeed = 5;
			@_animFrames = array<int8> = {0, 1, 0, 2};
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 11;
			preset.killAnim = 1;
			_supportsFire = true;
			_fireDelay = 200;
			_bullets.insertLast(_bulletPreferences(-4,1, 2,0, _bulletDirection::Either));
			_bullets.insertLast(_bulletPreferences( 4,1, 2,0, _bulletDirection::Either));
			preset.var[_objVar::DirectionCurrent] = 1;
		}
	}
	
	final class Fanolint_SuperTankety : Walker {
		Fanolint_SuperTankety(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.333333;
			preset.points = 20;
			preset.energy = 1;
			preset.animSpeed = 4;
			_direction = Directions::Left;
			_killAnimExplosion = true;
			_supportsFire = true;
			@_animFrames = array<int8> = {0,1};
			@_fireFrames = array<int8> = {2,2,2,2,2};
			_bullets.insertLast(_bulletPreferences(-4,0, 1,1, _bulletDirection::Left));
			_bullets.insertLast(_bulletPreferences( 4,0, 1,0, _bulletDirection::Right));
			_fireDelay = 100;
		}
	}
	
	
	final class Scraparap_GunnerDrone : StalkerGhost {
		Scraparap_GunnerDrone(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1;
			_direction = Directions::Left;
			@_animFrames = array<int8> = {0, 0, 0, 0, 1, 1};
			preset.killAnim = -2;
			preset.points = 20;
			preset.energy = 1;
			preset.animSpeed = 0;
			_supportsFire = true;
			_bullets.insertLast(_bulletPreferences(-8,0, 1,1, _bulletDirection::Left));
			_bullets.insertLast(_bulletPreferences( 8,0, 1,0, _bulletDirection::Right));
			_fireDelay = 100;
		}
		void onBehave(jjOBJ@ obj) override {
			if (obj.state == STATE::KILL && !_useJJ2DeathAnimation) {
				if (_deathSound >= 0)
					jjSample(obj.xPos, obj.yPos, SOUND::Sample(_deathSound));
				jjOBJ@ deathAnim = jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos, obj.objectID, CREATOR::OBJECT, BEHAVIOR::INACTIVE)];
				deathAnim.curFrame = obj.curFrame;
				deathAnim.killAnim = obj.killAnim;
				deathAnim.state = STATE::KILL;
				deathAnim.playerHandling = HANDLING::DYING;
				deathAnim.counterEnd = 0;
				deathAnim.behavior = jjVOIDFUNCOBJ(DyingDescent);
			}
			else StalkerGhost::onBehave(obj);
		}
		array<array<int>> DescentFrames = {
			{1,2,2},
			{1,4,4},
			{-1,6,6},
			{-1,6,8},
			{1,6,18},
			{1,8,20},
			{-1,8,23},
			{-1,8,32},
			{1,8,37},
			{1,8,50},
			{-1,16,64},
			{-1,16,80},
			{1,16,96},
			{1,16,117}
		};
		void DyingDescent(jjOBJ@ obj) {
			if ((jjGameTicks & 1) == 0) {
				if (obj.counterEnd < DescentFrames.length) {
					const auto@ newPosition = DescentFrames[obj.counterEnd++];
					obj.direction = newPosition[0];
					obj.xPos = obj.xOrg + newPosition[1] * 4 * _scaledObjectSpeed;
					obj.yPos = obj.yOrg + newPosition[2] * _scaledObjectSpeed;
				} else {
					Enemy::onBehave(obj);
				}
			}
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction);
		}
	}
	
	final class Scraparap_LaunchCart : Walker {
		Scraparap_LaunchCart(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.333333;
			preset.points = 20;
			preset.energy = 3;
			preset.animSpeed = 10;
			_direction = Directions::Left;
			_killAnimExplosion = true;
			_supportsFire = true;
			_bullets.insertLast(_bulletPreferences(-4,-3, 1,0, _bulletDirection::Either));
			_bullets.insertLast(_bulletPreferences( 4,-3, 1,0, _bulletDirection::Either));
			_fireDelay = 50;
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) override {
			if (bullet !is null)
				return Enemy::onObjectHit(obj,bullet,player,force);
			return true; //no collision damage
		}
		void myBehave(jjOBJ@ obj) const override {
			const auto lastX = obj.xPos, lastY = obj.yPos;
			Walker::myBehave(obj);
			obj.bePlatform(lastX,lastY);
		}
		void onBehave(jjOBJ@ obj) override {
			if (obj.state == STATE::KILL || obj.state == STATE::DEACTIVATE)
				obj.clearPlatform();
			Walker::onBehave(obj);
		}
	}
	
	final class Scraparap_RoboTurtleDrone : Walker {
		Scraparap_RoboTurtleDrone(const EnemyArguments &in arg) {
			super(arg);
			_speed = 2;
			_cliffReaction = CliffReaction::Fall;
			preset.points = 20;
			preset.energy = 1;
			preset.animSpeed = 3;
			_direction = Directions::Left;
		}
	}
	
	
	final class Megairbase_Doofusguard : Walker {
		Megairbase_Doofusguard(const EnemyArguments &in arg) {
			super(arg);
			_speed = 2;
			preset.points = 30;
			preset.energy = 3;
			preset.animSpeed = 4;
			_direction = Directions::Left;
			_killAnimExplosion = true;
			_supportsFire = true;
			_bullets.insertLast(_bulletPreferences(-12,0, 1,0, _bulletDirection::Left));
			_bullets.insertLast(_bulletPreferences( 12,0, 1,0, _bulletDirection::Right));
			_fireDelay = 100;
		}
	}
	
	final class Megairbase_Missile : Missile {
		Megairbase_Missile(const EnemyArguments &in arg) {
			super(arg);
			_speed = 4;
			_direction = Directions::FaceJazz;
			_killAnimExplosion = true;
			preset.points = 20;
			preset.energy = 1;
			preset.animSpeed = 3;
		}
	}
	
	final class Megairbase_SuperSpark : StalkerGhost {
		Megairbase_SuperSpark(const EnemyArguments &in arg) {
			super(arg);
			_speed = 2;
			_direction = Directions::Left;
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 5;
			preset.killAnim = 1;
			preset.points = 20;
			preset.energy = 1;
			preset.animSpeed = 0;
		}
	}
	
	
	final class Turtemple_JeTurtle : Enemy {
		Turtemple_JeTurtle(const EnemyArguments &in arg) {
			super(arg);
			preset.points = 20;
			preset.energy = 1;
			preset.animSpeed = 7;
			_killAnimExplosion = true;
			_supportsFire = true;
			_bullets.insertLast(_bulletPreferences(-12,0, 1,0, _bulletDirection::Left));
			_bullets.insertLast(_bulletPreferences( 12,0, 1,0, _bulletDirection::Right));
			_fireDelay = 100;
			_supportsDirection = true;
			_flipSpriteWhenMovingLeft = true;
			_direction = Directions::FaceJazz;
		}
		void myBehave(jjOBJ@ obj) const override {
			_adjustObjectDirection(obj);
			if (obj.state == STATE::START)
				obj.state = STATE::WAIT;
			else if (obj.state == STATE::BOUNCE)
				obj.yPos = jjWaterLevel;
			else if (obj.yPos >= jjWaterLevel - 10)
				obj.state = STATE::BOUNCE;
		}
	}
	
	final class Turtemple_ScorpWeenie : Walker {
		Turtemple_ScorpWeenie(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.333333;
			_cliffReaction = CliffReaction::Fall;
			@_animFrames = array<int8> = {0, 1, 0, 2};
			preset.points = 10;
			preset.energy = 1;
			preset.animSpeed = 3;
			_direction = Directions::Right;
		}
	}
	
	final class Nippius_SnowGoon : StandOnFloor {
		Nippius_SnowGoon(const EnemyArguments &in arg) {
			super(arg);
			preset.points = 10;
			preset.energy = 1;
			preset.animSpeed = 4;
			@_animFrames = array<int8> = {0, 1, 2, 1};
			_direction = Directions::Left;
			_supportsDirection = _flipSpriteWhenMovingLeft = true;
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 19;
			preset.killAnim = 1;
		}
	}
	
	final class Nippius_SkatePen : Walker {
		Nippius_SkatePen(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.333333;
			_cliffReaction = CliffReaction::Fall;
			preset.points = 10;
			preset.energy = 1;
			preset.animSpeed = 4;
			_direction = Directions::Left;
		}
	}
	
	final class Nippius_SkiTurtle : Walker {
		Nippius_SkiTurtle(const EnemyArguments &in arg) {
			super(arg);
			_speed = 4; //2 in level 2
			preset.points = 10;
			preset.energy = 1;
			preset.animSpeed = 3;
			_direction = Directions::Left;
		}
	}
	
	
	final class Jungrock_JetSnake : Walker {
		Jungrock_JetSnake(const EnemyArguments &in arg) {
			super(arg);
			_speed = 2;
			preset.points = 40;
			preset.energy = 1;
			preset.animSpeed = 3;
			_direction = Directions::Left;
			_killAnimExplosion = true;
		}
	}
	
	final class Jungrock_RedBuzzer : PathFollower {
		Jungrock_RedBuzzer(const EnemyArguments &in arg) {
			super(arg, 6);
			_speed = 2;
			_killAnimExplosion = true;
			preset.points = 10;
			preset.energy = 1;
			preset.animSpeed = 4;
		}
	}
	
	final class Jungrock_YellowBuzzer : Walker { //yellow buzzer also has a variant that seeks jazz out, but it appears exactly once across both levels
		Jungrock_YellowBuzzer(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.333333;
			preset.points = 50;
			preset.energy = 1;
			preset.animSpeed = 3;
			_direction = Directions::Left;
			_killAnimExplosion = true;
		}
	}
	
	
	final class Marbelara_Drageen : Snake {
		Marbelara_Drageen(const EnemyArguments &in arg) {
			super(arg, 7);
			_speed = 2;
			_killAnimExplosion = true;
			preset.points = 0;
			preset.energy = 3;
			_segmentSeparation = 5;
			preset.animSpeed = 4;
		}
	}
	
	final class Marbelara_Firebomb : Hopper {
		Marbelara_Firebomb(const EnemyArguments &in arg) {
			super(arg, 10, 10);
			preset.points = 20;
			preset.energy = 6;
			preset.animSpeed = 0;
			_killAnimExplosion = true;
		}
	}
	
	final class Marbelara_Schwarzenguard : Walker {
		Marbelara_Schwarzenguard(const EnemyArguments &in arg) {
			super(arg);
			_speed = 0.8f;
			preset.points = 50;
			preset.energy = 4;
			preset.animSpeed = 6;
			_direction = Directions::Left;
			_supportsFire = true;
			@_animFrames = array<int8> = {0,1,2,3,4,5};
			@_fireFrames = array<int8> = {6,6,6,6};
			_bullets.insertLast(_bulletPreferences(-8,0, 1,0, _bulletDirection::Left));
			_bullets.insertLast(_bulletPreferences( 8,0, 1,0, _bulletDirection::Right));
			_fireDelay = 100;
		}
	}
	
	
	final class Sluggion_Dragoon : Snake {
		Sluggion_Dragoon(const EnemyArguments &in arg) {
			super(arg, 8);
			_speed = 2;
			_killAnimExplosion = true;
			preset.points = 20;
			preset.energy = 5;
			_segmentSeparation = 5;
			preset.animSpeed = 6;
		}
	}
	
	final class Sluggion_RedBat : PathFollower {
		Sluggion_RedBat(const EnemyArguments &in arg) {
			super(arg, 9);
			_speed = 1.333333;
			_killAnimExplosion = true;
			preset.points = 50;
			preset.energy = 1;
			preset.animSpeed = 3;
		}
	}
	
	final class Sluggion_Sluggi : Walker {
		Sluggion_Sluggi(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1;
			_cliffReaction = CliffReaction::Fall;
			preset.points = 50;
			preset.energy = 1;
			preset.animSpeed = 4;
			_direction = Directions::Left;
		}
	}
	
	
	final class Dreempipes_Minite : Hopper {
		Dreempipes_Minite(const EnemyArguments &in arg) {
			super(arg, 3, 0);
			preset.points = 20;
			preset.energy = 3;
			preset.animSpeed = 7;
			preset.killAnim = 1;
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 5;
		}
	}
	
	final class Dreempipes_Overgrown : Hopper {
		Dreempipes_Overgrown(const EnemyArguments &in arg) {
			super(arg, 2, 0);
			preset.points = 30;
			preset.energy = 6;
			preset.animSpeed = 7;
			preset.killAnim = 1;
			_killAnimExplosion = true;
			_killAnimRepetitionCounts = 5;
		}
	}
	
	final class Dreempipes_TerrapinSwimmer : Walker {
		Dreempipes_TerrapinSwimmer(const EnemyArguments &in arg) {
			super(arg);
			_speed = 2;
			preset.points = 30;
			preset.energy = 3;
			preset.animSpeed = 6;
			_direction = Directions::Left;
		}
		void onBehave(jjOBJ@ obj) override {
			if (
				(obj.yPos >= jjWaterLevel)
				//(jjGameTicks % 280 < 210)
				|| obj.state == STATE::START || obj.state == STATE::DEACTIVATE || obj.state == STATE::FREEZE || obj.state == STATE::KILL
			)
				Walker::onBehave(obj);
			else //sleeping above water
				obj.curFrame = jjAnimations[obj.curAnim + 1].firstFrame + ((((obj.var[_objVar::AnimCounter] = obj.var[_objVar::AnimCounter] + 1) >> 1) / 7) % 5);
		}
	}
	
	
	final class Pezrox_ClammyHorizontal : Floater {
		Pezrox_ClammyHorizontal(const EnemyArguments &in arg) {
			super(arg);
			_speed = 2;
			preset.points = 30;
			preset.energy = 1;
			preset.animSpeed = 0;
			_direction = Directions::Left;
		}
	}
	
	final class Pezrox_ClammyVertical : Enemy {
		Pezrox_ClammyVertical(const EnemyArguments &in arg) {
			super(arg);
			_supportsSpeed = _supportsDirection = true;
			_speed = 1;
			preset.points = 30;
			preset.energy = 1;
			preset.animSpeed = 0;
			_direction = Directions::Left;
		}
		void myBehave(jjOBJ@ obj) const override {
			if (obj.state == STATE::START) {
				_adjustObjectDirection(obj);
				obj.direction = (obj.var[_objVar::DirectionCurrent] == -1) ? SPRITE::FLIPNONE : SPRITE::FLIPV;
				obj.state = STATE::FLY;
			}
			
			const int direction = obj.var[_objVar::DirectionCurrent];
			const jjANIMFRAME@ animFrame = jjAnimFrames[obj.curFrame];
			const int positionForwards = int(obj.yPos + direction * animFrame.height / 2);
			
			if (
				positionForwards < 0 ||
				(jjEventGet(int(obj.xPos) / 32, positionForwards / 32) == AREA::STOPENEMY) ||
				(jjMaskedHLine(int(obj.xPos + animFrame.hotSpotX), animFrame.width, positionForwards)) || //wall
				positionForwards >= _levelHeight
			) {
				_reverseDirection(obj);
				obj.direction ^= 0x40; //vertically flipped
			} else
				obj.yPos += direction * obj.xAcc;
		}
	}
	
	final class Pezrox_GreenSnake : Walker {
		Pezrox_GreenSnake(const EnemyArguments &in arg) {
			super(arg);
			_speed = 2;
			preset.points = 20;
			preset.energy = 1;
			preset.animSpeed = 4;
			_direction = Directions::Left;
			_killAnimExplosion = true;
		}
	}
	
	
	final class Crysilis_GoldenBounceSpike : Enemy {
		Crysilis_GoldenBounceSpike(const EnemyArguments &in arg) {
			super(arg);
			preset.points = 40;
			preset.energy = 5;
		}
		void myBehave(jjOBJ@ obj) const override {
			int xOffset = (obj.var[_objVar::AnimCounter] >> 3) % 6;
			if (xOffset > 2) xOffset = 5 - xOffset; //0, 1, 2, 2, 1, 0
			obj.xPos = obj.xOrg - (xOffset << 2) * _scale;
		}
	}
	
	final class Crysilis_LooGuard : PathFollower {
		Crysilis_LooGuard(const EnemyArguments &in arg) {
			super(arg, 10);
			_speed = 1.333333;
			preset.points = 20;
			preset.energy = 1;
			preset.animSpeed = 6;
			_supportsFire = true;
			@_animFrames = array<int8> = {0,1,2,3};
			@_fireFrames = array<int8> = {4,4,4,5,5,5};
			_bullets.insertLast(_bulletPreferences(-4,2, 1,0, _bulletDirection::Left));
			_bullets.insertLast(_bulletPreferences( 4,2, 1,0, _bulletDirection::Right));
			_fireDelay = 254;
		}
	}
	
	
	final class Battleships_ArmorDoofi : Walker {
		Battleships_ArmorDoofi(const EnemyArguments &in arg) {
			super(arg);
			_speed = 4/float(6+1);
			preset.points = 30;
			preset.energy = 8;
			preset.animSpeed = 6;
			_killAnimExplosion = true;
		}
	}
	
	final class Battleships_BounceSpike : Hopper {
		Battleships_BounceSpike(const EnemyArguments &in arg) {
			super(arg, 5, 0);
			preset.points = 30;
			preset.energy = 10;
			preset.animSpeed = 3;
			_killAnimExplosion = true;
		}
	}
	
	final class Battleships_Generator : Enemy {
		Battleships_Generator(const EnemyArguments &in arg) {
			super(arg);
			preset.points = 60;
			preset.energy = 5;
			preset.animSpeed = 3;
			preset.var[_objVar::DirectionCurrent] = 1;
			_supportsFire = true;
			_bullets.insertLast(_bulletPreferences(-8,-5, 1,0, _bulletDirection::Either, 1));
			_bullets.insertLast(_bulletPreferences( 8,-5, 1,0, _bulletDirection::Either, 1));
			_fireDelay = 80;
		}
		bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) override {
			if (bullet !is null)
				return Enemy::onObjectHit(obj,bullet,player,force);
			return true; //no collision damage
		}
		void onBehave(jjOBJ@ obj) override {
			if (obj.state != STATE::KILL || _useJJ2DeathAnimation)
				Enemy::onBehave(obj);
			else { //no death animation at all
				if (_deathSound >= 0)
					jjSample(obj.xPos, obj.yPos, SOUND::Sample(_deathSound));
				obj.delete();
			}
		}
		void onDraw(jjOBJ@ obj) {
			if (obj.isActive)
				_drawBodyFrame(
					obj, obj.xPos, obj.yPos, obj.curFrame, 1,
					3 //draw to foreground
				);
		}
	}
	
	final class Battleships_SuperBee : PathFollower {
		Battleships_SuperBee(const EnemyArguments &in arg) {
			super(arg, 11);
			_speed = 2;
			preset.points = 20;
			preset.energy = 1;
			preset.animSpeed = 3;
			_supportsFire = true;
			_bullets.insertLast(_bulletPreferences(-4,-1, 1,0, _bulletDirection::Left, 1));
			_bullets.insertLast(_bulletPreferences( 4,-1, 1,0, _bulletDirection::Right, 1));
			_fireDelay = 100;
			_killAnimExplosion = true;
		}
	}
	
	
	final class Holidaius_BlueDog : Walker {
		Holidaius_BlueDog(const EnemyArguments &in arg) {
			super(arg);
			_speed = 1.333333;
			preset.points = 30;
			preset.energy = 3;
			preset.animSpeed = 4;
			_direction = Directions::Left;
			_killAnimExplosion = true;
			_supportsFire = true;
			_fireAnimID = 1;
			@_fireFrames = array<int8> = {0,0, 1,1,2,2, 1,1,2,2, 1,1,2,2, 1,1,2,2};
			_fireDelay = 150;
		}
	}
	
	final class Holidaius_Devil : PathFollower {
		Holidaius_Devil(const EnemyArguments &in arg) {
			super(arg, 3);
			_speed = 1.333333;
			_killAnimExplosion = true;
			preset.points = 50;
			preset.energy = 3;
			preset.animSpeed = 3;
		}
	}
	
	final class Holidaius_HandHorizontal : WallStickerHorizontal {
		Holidaius_HandHorizontal(const EnemyArguments &in arg) {
			super(arg);
			@_animFrames = array<int8> = {0, 1, 2, 3, 4, 5, 6,6,6,6,6,6,6,6,6,6,6,6,6};
			preset.animSpeed = 5;
			preset.points = 60;
			preset.energy = 1;
			_killAnimExplosion = true;
		}
	}
	final class Holidaius_HandVertical : WallStickerVertical {
		Holidaius_HandVertical(const EnemyArguments &in arg) {
			super(arg);
			@_animFrames = array<int8> = {0, 1, 2, 3, 4, 5, 6,6,6,6,6,6,6,6,6,6,6,6,6};
			preset.animSpeed = 5;
			preset.points = 60;
			preset.energy = 1;
			_killAnimExplosion = true;
		}
	}
	
	final class Holidaius_SkiTurtle : Walker {
		Holidaius_SkiTurtle(const EnemyArguments &in arg) {
			super(arg);
			_speed = 2;
			preset.points = 50;
			preset.energy = 1;
			preset.animSpeed = 3;
			_direction = Directions::Left;
			_killAnimExplosion = true;
		}
	}
	
	final class Holidaius_SnowMonkey : Walker {
		Holidaius_SnowMonkey(const EnemyArguments &in arg) {
			super(arg);
			_speed = 4;
			preset.points = 30;
			preset.energy = 1;
			preset.animSpeed = 4;
			_direction = Directions::Left;
			_killAnimExplosion = true;
			_supportsFire = true;
			@_fireFrames = array<int8> = {5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5};
			_fireDelay = 50;
		}
	}
}

#pragma require "Jazz1Enemies v05.asc"
#pragma require "Jazz1Enemies v05.j2a"
#include "Resize v11.asc"