Downloads containing TrueColor v13.asc

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Anniversary Bash 26 Battle Jazz2Online Battle N/A Download file
JJ2+ Only: Anniversary Bash 25 Battle Jazz2Online Battle N/A Download file
TSF with JJ2+ Only: Asteroid Armada PurpleJazz Battle N/A Download file
JJ2+ Only: Spaceships (v1.01) PurpleJazz Custom / Concept N/A Download file
TSF with JJ2+ Only: Anniversary Bash 24 Battle Jazz2Online Battle N/A Download file
TSF with JJ2+ Only: Acid ReignFeatured Download cooba Battle 9.2 Download file
TSF with JJ2+ Only: Jazz of the JungleFeatured Download Violet CLM Single player 8.9 Download file
TSF with JJ2+ Only: Anniversary Bash 23 levels Jazz2Online Multiple N/A Download file
TSF with JJ2+ Only: Goliath WoodsFeatured Download PurpleJazz Capture the flag 8.9 Download file
Jazz 1 Enemies Violet CLM Other N/A Download file
TrueColor and Resize Violet CLM Other N/A Download file

File preview

//TrueColor v13.asc: version 1.3 (2019/05/04)
#pragma require "TrueColor v13.asc"

/*
TrueColor v13.asc lets scriptwriters create palette-independent images to be drawn to the screen as sprites, meaning you can script an enemy, pickup, etc. that uses colors that are nowhere to be found in the level's palette. This is accomplished over a series of three steps, repeatable as many times as you like:
	1) Load an external .png (or .bmp) file from the player's harddrive into memory in a TrueColor::Bitmap object.
	2) Call TrueColor::Bitmap::saveToAnimFrames one or more times, or simplify by using the TrueColor::AllocateSpriteSheet function, to save one or more rectangular subareas from the image into one or more series of subsequent jjANIMFRAMEs, using one or more TrueColor::Coordinates objects to specify how much of the image to save.
	3) Use the various TrueColor drawing functions (NOT the normal jjDraw* functions or jjCANVAS::draw* methods) to draw the resulting images, as stored in jjANIMFRAMEs series, to the screen.

API:
	const uint TrueColor::NumberOfFramesPerImage
		Each TrueColor image, in order to be usable by drawing code, is saved to a series of (TrueColor::NumberOfFramesPerImage) subsequent jjANIMFRAME objects. You will need to be able to multiply and/or divide by this number when dealing with any animation more than one image long. For instance, if you want to draw the third(2) image from a TrueColor animation stored as the first(0) animation in ANIM::SPARK:
			TrueColor::DrawSprite(100, 100, ANIM::SPARK, 0, 2 * TrueColor::NumberOfFramesPerImage);
	
	
	ANIM::Set TrueColor::FindCustomAnim()
		A convenience function not used directly by any part of the TrueColor library, TrueColor::FindCustomAnim simply finds you an ANIM::Set constant such that allocating a new animset to it would not interfere with any existing code running in the level.
	
	void TrueColor::ProcessPalette()
		This function must be called at the beginning of the level and again any time jjPAL::apply is called. This is because the TrueColor images, while visually palette-independent, must still use the contents of jjPalette in order to set themselves up as jjANIMFRAME images, and the details of this process can change slightly every time the palette is edited. As a result, constant jjPalette changes are an even worse idea in levels with TrueColor images than they are the rest of the time.
		
	void TrueColor::EnableCaching(bool enable = true)
		Enabling caching has two separate effects. First, every time you save a (subarea of a) bitmap image to some jjANIMFRAMEs (either directly through TrueColor::Bitmap::saveToAnimFrames or indirectly through TrueColor::AllocateSpriteSheet), TrueColor v13.asc will cache what image data was saved to which frames. Second, every time TrueColor::ProcessPalette is called, all cached image data will be saved to their respective animframes again, using the new palette. If caching is NOT enabled when ProcessPalette() is called, all previously saved TrueColor images must be MANUALLY saved to their respective jjANIMFRAMEs all over again, though the actual animset allocation need not be repeated.
		
		Note that the resaving of the cached images is done in response to TrueColor::ProcessPalette, not in response to jjPAL::apply, which may present difficulties if jjPAL::apply is called by a script module other than the one including TrueColor v13.asc. At worst you can try using jjPAL::operator==.
		
		Disabling caching (i.e. passing "false") will not empty the existing cache, it will only prevent the cache from being added to or used until such time as caching is enabled again. As a general rule, you should enable caching iff your script includes one or more palette changes, so that you don't have to do that work manually, but it will serve no purpose (and only take up unneeded memory) if your palette never changes.
	
	void TrueColor::AllocateSpriteSheet(const ANIM::Set setID, const TrueColor::Bitmap& bitmap, const array<array<TrueColor::Coordinates>>& setCoordinates = array<array<TrueColor::Coordinates>>(1, array<TrueColor::Coordinates>(1)))
		An all-in-one function that 1) allocates an entire new animset (at jjAnimSets[setID]) with its length and the lengths of each of its animations determined by the sizes of setCoordinates, then 2) saves all bitmap's rectangular subareas (defined by setCoordinates' various TrueColor::Coordinates objects) into the jjANIMFRAMEs of that animset. This is almost the same thing as jjANIMSET::load, with the caveats that a) all the sprite properties are defined in the script instead of in a .j2a file, and b) you must remember to use TrueColor::NumberOfFramesPerImage when browsing frames within any animations so allocated.
		
		See example script for example.
	
	void TrueColor::DrawSprite(float xPixel, float yPixel, int setID, uint8 animation, uint frame, int direction = 0, uint8 layerZ = 4, uint8 layerXY = 4, int8 playerID = -1)
	void TrueColor::DrawCanvasSprite(jjCANVAS@ canvas, int xPixel, int yPixel, int setID, uint8 animation, uint frame, int8 direction = 0)
	...
		All single sprite drawing functions--Sprite, ResizedSprite, RotatedSprite, and SwingingVineSprite, both with and without FromCurFrame--are recreated in TrueColor. The argument pattern is always exactly the same as the normal JJ2+ version's, except for the spriteMode and spriteParam arguments, which are omitted entirely. To call the TrueColor equivalent of a jjCANVAS method, insert "Canvas" after "Draw" and pass the jjCANVAS@ as the first argument.
		
		The sprite's opacity will be drawn at only half opacity iff jjANIMFRAME::transparent is true for the first jjAnimFrames entry in the sequence of (TrueColor::NumberOfFramesPerImage) entries. This can be accomplished by setting TrueColor::Coordinates::transparent to true while saving them.
		
		The sprite should be saved with TrueColor::Coordinates::swingingVine as true iff you intend to use any functions with "SwingingVineSprite" in their name on it. Otherwise it will be drawn incorrectly, just as with normal, none-TrueColor swinging vine sprites.
		
		There are no TrueColor versions of Pixel, Rectangle, or String (at this time).
	
	void TrueColor::DrawObject(jjOBJ@)
		A TrueColor version of jjOBJ::draw(), for convenience purposes. Frozen objects will be drawn as frozen (SPRITE::FROZEN), recently hit objects will be drawn all white (SPRITE::SINGLECOLOR), and all other objects will be drawn using a call to TrueColor::DrawSpriteFromCurFrame. No equivalent to SPRITE::GEM is currently provided.
	
	
	class TrueColor::AlphaColor
		Almost identical to jjPALCOLOR, but with an added uint8 alpha property, ranging from 0 (invisible) to 255 (fully opaque). The two classes are otherwise completely interoperable and replacing "jjPALCOLOR" with "TrueColor::AlphaColor" should work in all cases.
	
	class TrueColor::Bitmap
		A Bitmap is roughly analogous to a jjPIXELMAP, in that it contains an array of pixel colors--here represented by TrueColor::AlphaColor objects, rather than uint8s--and can be saved to jjANIMFRAMEs. However, its most important constructors are from a string or jjSTREAM, loading a 2D image (traditionally originating from an external .png or .bmp file) into memory, and it cannot be saved to tiles or textured backgrounds.
		
		Properties:
			const uint width
			const uint height
			const bool usesAlphaChannel
				If true, each pixel will be drawn with opacity based on its TrueColor::AlphaColor.alpha value.
				If false, all pixels will be drawn fully opaque except for those whose rgb values are all 0, which will be fully invisible.
			array<array<AlphaColor>> pixels
				All the 32-bit colors in the image, accessed pixels[x][y]
		Methods:
			Bitmap()
				Default constructor is not helpful but required by AngelScript: width and height are 0 and pixels is length 0
			Bitmap(uint w, uint h, bool ac)
				Constructor from dimensions: sets width and height to w and h and resizes pixels accordingly, filling the arrays with transparent AlphaColor objects (red==0, green==0, blue==0, alpha==0), and sets usesAlphaChannel to ac.
			Bitmap(const string &in fileroot)
				Tries to load fileroot as a 32-bit image file. Any failure results in width/height both equalling 0, pixels remaining at length 0, and a variously helpful error message printed to the chatlog; otherwise, the pixels array is filled with the contents of the image file. Acceptable file formats:
					Almost any noninterlaced .png file
					A 24-bit .bmp file with a width that is a multiple of four pixels 
				If no file extension is included, ".bmp" will be appended for backwards compatibility, but .png files are superior in every way so you should use those instead. If the loaded image is 24-bit, pixels of color 0,0,0 will be given alpha==0 and all other pixels will be given alpha==255.
			Bitmap(jjSTREAM &in file)
				Tries to load this jjSTREAM as a 32-bit image file with the exact same internal formatting as the file loaded by the previous constructor. There should be no difference between writing "TrueColor::Bitmap bar('foo.bmp');" and writing "TrueColor::Bitmap bar(jjSTREAM('foo.bmp'));" This constructor, however, is more useful in case the image does not come directly from the user's harddrive, e.g. if it is sent from the server to a client in a multiplayer game.
			Bitmap(const jjPIXELMAP &in pixelmap, const jjPAL &in palette = jjPalette)
				Constructs an image with the same dimensions and image as the pixelmap, where for each coordinate pair x,y, bitmap.pixels[x][y] = palette.color[pixelmap[x,y]].
			Bitmap(const TrueColor::Bitmap &in source, uint left, uint top, uint width, uint height)
				First constricts left/top/width/height as needed if they do not actually fit into source's dimensions, then resizes the pixels array to width,height and fills it with the contents of the defined subarea of source. In other words, this is a cropping constructor.
			void saveToAnimFrames(uint frameID, const TrueColor::Coordinates@ coordinates = null) const
				Saves this image, or a subarea within it, to a series of subsequent jjANIMFRAMEs beginning at jjAnimFrames[frameID] and continuing until jjAnimFrames[frameID + TrueColor::NumberOfFramesPerImage]; sets all those jjANIMFRAMEs' hotspot, gunspot, coldspot, and transparency properties according to the equivalent properties on the coordinates argument; and picks the jjANIMFRAMEs' transparency colors based on the coordinates argument's swingingVine property. If the coordinates argument is left null (default), the _entire_ image will be saved to the jjANIMFRAMEs, not a subarea of it, and their hotspots/gunspots/coldspots will all be left at 0,0.
			void swizzle(COLOR::Component red, COLOR::Component green, COLOR::Component blue)
				A shortcut method to apply AlphaColor::swizzle to every pixel in the image.
			void generateAlphaChannel()
				Normally, the value of usesAlphaChannel will be dependent on what kind of information was passed to the object's constructor, e.g. a .bmp file will never have an alpha channel, but many .png files will. Calling this method will manually set usesAlphaChannel to true, meaning that if you make any manual changes to any AlphaColor::alpha values, they will show up upon saving this image to jjANIMFRAMEs.
			void removeAlphaChannel(uint8 threshold = 127)
				Sets usesAlphaChannel to false, and makes transparent any pixels whose AlphaColor::alpha values are lower than the threshold value.
	
	class TrueColor::Coordinates
		Roughly analogous to jjANIMFRAME, in that it provides (a series of) jjANIMFRAMEs with their dimensions and hotspot/gunspot/coldspot positions, but its primary purpose is to specify a rectangular subarea of a TrueColor::Bitmap to be saved to jjANIMFRAMEs. For example, a TrueColor::Bitmap with width==100 and height==50, saved to jjANIMFRAMEs using a TrueColor::Coordinates with left=50,top=25,width=50,height=25 would save only the bottom right quadrant of its entire 100x50 image.
		
		Properties:
			int hotSpotX, hotSpotY, gunSpotX, gunSpotY, coldSpotX, coldSpotY
			bool transparent, swingingVine
		Methods:
			Coordinates()
				Default constructor sets all properties, including dimensions, to 0
			Coordinates(uint l, uint t, uint w, uint h, int hX = 0, int hY = 0, int gX = 0, int gY = 0, int cX = 0, int cY = 0, bool tr = false, bool sv = false)
				Sets dimensions according to the first four arguments (left, top, width, height), then optionally allows you to set all the public properties at the same time as constructing the object
			
WARNING: All internal code details are subject to change, and any properties, methods, or functions NOT described above should not be used by external scripts for risk of failing to work in later updates to this library.
*/


















namespace TrueColor {
	const uint NumberOfFramesPerImage = 4; //only 3 are needed for non-alpha-channel images, but consistency is better for the user
	
	ANIM::Set FindCustomAnim() { //convenience
		uint customAnimID = 0;
		while (jjAnimSets[ANIM::CUSTOM[customAnimID]].firstAnim != 0) { //A loaded anim set will have a firstAnim value of 1 or higher.
			customAnimID += 1; //Keep searching...
			if (customAnimID == 256) {
				jjDebug("No free animation sets found!");
				return ANIM::SPAZ;
			}
		}
		
		return ANIM::CUSTOM[customAnimID];
	}
	
	bool _caching = false;
	dictionary _savedBitmapsCache;
	void EnableCaching(bool enable = true) {
		_caching = enable;
	}
	
	array<uint8> __NearestColors(256);
	void ProcessPalette() {
		array<uint8> brightnesses(256);
		for (uint i = 10; i <= 245; ++i) { //recreate JJ2's internal brightness values, which are what it uses for SPRITE::NEONGLOW
			const jjPALCOLOR color = jjPalette.color[i];
			brightnesses[i] = (7499 * color.blue + 38446 * color.green + 19591 * color.red) >> 16;
		}
		
		for (int i = 0; i < 256; ++i) {
			int closestDistance = 1000;
			uint8 closestDistanceID = 0;
			for (int j = 10; j <= 245; ++j) {
				if (j == 128) //don't use color 128, because that's used by swinging vines for transparency
					continue;
				const int distance = int(abs(i - brightnesses[j]));
				if (distance < closestDistance) {
					closestDistance = distance;
					closestDistanceID = j;
				}
			}
			__NearestColors[i] = closestDistanceID;
		}
		
		if (_caching) {
			const auto keys = _savedBitmapsCache.getKeys();
			for (uint i = 0; i < keys.length; ++i) {
				const auto key = keys[i];
				cast<Bitmap>(_savedBitmapsCache[key]).saveDirectlyToFrames(parseUInt(key.substr(2)), key[0] == 'T'[0], key[1] == 'T'[0]);
			}
		}
	}

	uint _reverseEndianness32(uint value) {
		return
			(value >> 24) |
			(((value >> 16) & 0xFF) << 8) |
			(((value >> 8) & 0xFF) << 16) |
			(((value) & 0xFF) << 24);
	}
	
	class Bitmap {
		private uint _width = 0, _height = 0;
		private bool _hasAlpha = false;
		uint width {
			get const { return _width; }
		};
		uint height {
			get const { return _height; }
		};
		bool usesAlphaChannel {
			get const { return _hasAlpha; }
		}
		array<array<AlphaColor>> pixels;
		
		Bitmap() { pixels.resize(0); }
		private void resize(uint w, uint h) {
			_width = w;
			_height = h;
			pixels = array<array<AlphaColor>>(width, array<AlphaColor>(height));
		}
		Bitmap(uint w, uint h, bool p = false) {
			resize(w,h);
			_hasAlpha = p;
		}
		Bitmap(const string &in fileroot) {
			string filename = fileroot;
			if (!jjRegexMatch(filename.substr(filename.length - 4), "\\.(bmp|png)", true)) //case-insensitive match
				filename += ".bmp";
			jjSTREAM file(filename);
			if (file.isEmpty()) {
				jjDebug("File " + filename + " not found!");
				return;
			}
			_loadFromStream(file);
		}
		Bitmap(jjSTREAM &in file) {
			if (file.isEmpty()) {
				jjDebug("Cannot create a Bitmap from an empty stream.");
				return;
			}
			_loadFromStream(file);
		}
		protected void _loadFromStream(jjSTREAM &in file) {
			uint32 imageWidth, imageHeight;
			//determine file format
			uint16 headerField; file.pop(headerField);
			if (headerField == 0x4D42) { //BMP
				_hasAlpha = false;
				{ //Bitmap file header
					uint32 fileSize; file.pop(fileSize);
					if (fileSize != file.getSize() + 6) { //6 for bytes already read
						jjDebug("Invalid internal filesize");
						return;
					}
					file.discard(8);
				}
				{ //DIB header
					uint32 headerType; file.pop(headerType);
					if (headerType != 40) { //"BITMAPINFOHEADER"
						jjDebug("Unsupported BMP format");
						return;
					}
					file.pop(imageWidth);
					file.pop(imageHeight);
					uint16 colorPlanes; file.pop(colorPlanes);
					if (colorPlanes != 1) {
						jjDebug("Invalid file");
						return;
					}
					uint16 bitDepth; file.pop(bitDepth);
					if (bitDepth != 24) {
						jjDebug("Unsupported BMP format");
						return;
					}
					uint32 compressionMethod; file.pop(compressionMethod);
					if (compressionMethod != 0) {
						jjDebug("Unsupported BMP format");
						return;
					}
					uint32 imageSize; file.pop(imageSize);
					if (imageSize != 0 && imageSize != imageWidth * imageHeight * 3) {
						jjDebug("Unsupported file (width must be multiple of 4 pixels)");
						return;
					}
					file.discard(8); //resolution weirdness
					uint32 colorCount; file.pop(colorCount);
					if (colorCount != 0) {
						jjDebug("Unsupported BMP format");
						return;
					}
					uint32 importantColorCount; file.pop(importantColorCount);
					if (importantColorCount != 0) {
						jjDebug("Unsupported BMP format");
						return;
					}
				}
				resize(imageWidth, imageHeight);
				for (int y = imageHeight - 1; y >= 0; --y)
					for (uint x = 0; x < imageWidth; ++x) {
						jjPALCOLOR color;
						file.pop(color.blue); file.pop(color.green); file.pop(color.red);
						pixels[x][y] = color;
					}
			} else if (headerField == 0x5089) {//PNG
				const array<uint8> DesiredHeaderBytes = {0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
				for (uint i = 0; i < DesiredHeaderBytes.length; ++i) {
					uint8 headerByte; file.pop(headerByte);
					if (headerByte != DesiredHeaderBytes[i]) {
						jjDebug("Invalid PNG header");
						return;
					}
				}
				//after this we get to the chunks
				jjSTREAM@ Header = null, Palette = null;
				array<AlphaColor> InternalPalette = array<AlphaColor>(256);
				jjSTREAM Data;
				jjSTREAM@ tRNS = jjSTREAM();
				while (true) {
					uint chunkLength; file.pop(chunkLength);
					jjSTREAM chunk; file.get(chunk, _reverseEndianness32(chunkLength) + 4);
					uint chunkCRC; file.pop(chunkCRC);
					if (jjCRC32(chunk) != _reverseEndianness32(chunkCRC)) {
						string chunkTitle; chunk.get(chunkTitle, 4);
						jjDebug('PNG CRC check for ' + chunkTitle + ' incorrect');
						return;
					}
					string chunkTitle; chunk.get(chunkTitle, 4);
					if (chunkTitle == "IHDR")
						@Header = @chunk;
					else if (chunkTitle == "PLTE")
						@Palette = @chunk;
					else if (chunkTitle == "IDAT")
						Data.write(chunk);
					else if (chunkTitle == "tRNS") //optional
						@tRNS = chunk;
					else if (chunkTitle == "IEND")
						break;
					//else this is unimportant
				}
				
				Header.pop(imageWidth); imageWidth = _reverseEndianness32(imageWidth);
				Header.pop(imageHeight); imageHeight = _reverseEndianness32(imageHeight);
				uint8 bitDepth; Header.pop(bitDepth);
				if (bitDepth != 8) { //for now?
					jjDebug("Unsupported PNG format (not enough colors)");
					return;
				}
				uint8 colorType; Header.pop(colorType);
				uint numberOfBytesPerPixel;
				const uint tRNSsize = tRNS.getSize();
				jjPALCOLOR tRNScolor; //for ColorType 0 or 2, the sole color that is transparent
				switch (colorType) {
					case 0: //grayscale
						numberOfBytesPerPixel = 1;
						if (tRNSsize == 2) {
							tRNS.pop(tRNScolor.green); //first byte is useless but has to get popped to somewhere
							tRNS.pop(tRNScolor.red); //index
							_hasAlpha = true;
						} else if (tRNSsize != 0) jjDebug("Warning: tRNS chunks in PNG images with Color Type 0 must be 2 bytes long");
						break;
					case 2: //truecolor
						numberOfBytesPerPixel = 3;
						if (tRNSsize == 6) {
							uint16 r,g,b;
							tRNS.pop(r);
							tRNS.pop(g);
							tRNS.pop(b);
							tRNScolor = jjPALCOLOR(r >> 8, g >> 8, b >> 8); //big-indian
							_hasAlpha = true;
						} else if (tRNSsize != 0) jjDebug("Warning: tRNS chunks in PNG images with Color Type 2 must be 6 bytes long");
						break;
					case 3: //paletted
						numberOfBytesPerPixel = 1;
						_hasAlpha = tRNSsize != 0;
						break;
					case 4: //grayscale with alpha
						numberOfBytesPerPixel = 2;
						_hasAlpha = true; //but stored in the image, not in tRNS
						if (tRNSsize != 0) jjDebug("Warning: tRNS chunks should not appear in PNG images with Color Type 4.");
						break;
					case 6: //truecolor with alpha 
						numberOfBytesPerPixel = 4;
						_hasAlpha = true; //but stored in the image, not in tRNS
						if (tRNSsize != 0) jjDebug("Warning: tRNS chunks should not appear in PNG images with Color Type 6.");
						break;
					default:
						jjDebug("Invalid color format");
						return;
				}
				{
					uint8 compressionMethod; Header.pop(compressionMethod);
					if (compressionMethod != 0) {
						jjDebug("Unsupported PNG format (too compressed)");
						return;
					}
					uint8 filterMethod; Header.pop(filterMethod);
					if (filterMethod != 0) {
						jjDebug("Unsupported PNG format (invalid filter)");
						return;
					}
					uint8 interlaceMethod; Header.pop(interlaceMethod);
					if (interlaceMethod != 0) {
						jjDebug("Unsupported PNG format (interlaced)");
						return;
					}
				}
				
				if (colorType & 2 == 0) //grayscale
					for (uint i = 0; i < 256; ++i)
						InternalPalette[i] = AlphaColor(i,i,i, i != tRNScolor.red ? 255 : 0);
				else if (Palette !is null)
					for (uint i = 0; i < 256; ++i) {
						AlphaColor color;
						Palette.pop(color.red);
						Palette.pop(color.green);
						Palette.pop(color.blue);
						if (!tRNS.isEmpty())
							tRNS.pop(color.alpha);
						else
							color.alpha = 255;
						InternalPalette[i] = color;
						if (Palette.isEmpty()) break;
					}
				
				jjSTREAM uncompressed;
				if (!jjZlibUncompress(Data, uncompressed, imageHeight * (1 + imageWidth * (numberOfBytesPerPixel)))) { //1=filter, per row
					jjDebug("Error decompressing PNG image data");
					return;
				}
				
				resize(imageWidth, imageHeight);
				array<array<array<uint8>>> image(imageWidth+1, array<array<uint8>>(imageHeight+1, array<uint8>(numberOfBytesPerPixel, 0))); //+1 because the top row/left column need to draw from empty data

				for (uint y = 0; y < imageHeight; ++y) {
					uint8 filter; uncompressed.pop(filter);
					for (uint x = 0; x < imageWidth; ++x) {
						array<uint8>@ source = image[x+1][y+1];
						for (uint z = 0; z < numberOfBytesPerPixel; ++z) {
							uint8 cc; uncompressed.pop(cc);
							uint8 predictedValue;
							switch (filter) {
								case 0: //None
									predictedValue = 0;
									break;
								case 1: //Sub
									predictedValue = image[x][y+1][z];
									break;
								case 2: //Up 
									predictedValue = image[x+1][y][z];
									break;
								case 3: //Average
									predictedValue = uint8((uint(image[x][y+1][z]) + uint(image[x+1][y][z])) >> 1);
									break;
								case 4: { //Paeth
									const int
										a = image[x][y+1][z],
										b = image[x+1][y][z],
										c = image[x][y][z],
										p = a + b - c,
										pa = int(abs(p - a)),
										pb = int(abs(p - b)),
										pc = int(abs(p - c));
									if (pa <= pb && pa <= pc)
										predictedValue = uint8(a);
									else if (pb <= pc)
										predictedValue = uint8(b);
									else
										predictedValue = uint8(c);
									break; }
								default:
									jjDebug("Invalid PNG filter value " + filter);
									break;
							}
							source[z] = cc + predictedValue; //get difference
						}
						AlphaColor@ dest = @pixels[x][y];
						switch (colorType) {
							case 0: //grayscale
							case 3: //paletted
								dest = InternalPalette[source[0]];
								break;
							case 2: //truecolor
								dest.red = source[0];
								dest.green = source[1];
								dest.blue = source[2];
								dest.alpha = dest != tRNScolor ? 255 : 0;
								break;
							case 4: //grayscale with alpha
								dest = InternalPalette[source[0]];
								dest.alpha = source[1];
								break;
							case 6: //truecolor with alpha
								dest.red = source[0];
								dest.green = source[1];
								dest.blue = source[2];
								dest.alpha = source[3];
								break;
						}
					}
				}
			} else {
				jjDebug('Invalid file header (should be "BM" or " PNG")');
				return;
			}
		}
		Bitmap(const jjPIXELMAP &in pixelmap, const jjPAL &in palette = jjPalette) {
			resize(pixelmap.width, pixelmap.height);
			for (int x = _width - 1; x >= 0; --x) {
				const auto@ column = pixels[x];
				for (int y = _height - 1; y >= 0; --y)
					column[y] = palette.color[pixelmap[x,y]];
			}
		}
		Bitmap(const Bitmap &in source, uint left, uint top, uint imageWidth, uint imageHeight) {
			if (left >= source.width)
				return;
			if (top >= source.height)
				return;
			if (left + imageWidth > source.width) imageWidth = source.width - left;
			if (top + imageHeight > source.height) imageHeight = source.height - top;
			resize(imageWidth, imageHeight);
			_hasAlpha = source._hasAlpha;
			for (int x = imageWidth - 1; x >= 0; --x) {
				const auto@ col1 = pixels[x];
				const auto@ col2 = source.pixels[left + x];
				for (int y = imageHeight - 1; y >= 0; --y)
					col1[y] = col2[top + y];
			}
		}
		
		void saveDirectlyToFrames(uint frameID, bool halfOpacity = false, bool swingingVine = false) const {
			array<jjPIXELMAP@> maps(NumberOfFramesPerImage, null);
			for (uint c = 0; c < NumberOfFramesPerImage; ++c)
				@maps[c] = @jjPIXELMAP(width, height);
				
			if (!_hasAlpha) { //24-bit
				const jjPALCOLOR transparent;
				for (int x = _width - 1; x >= 0; --x) {
					const auto@ column = pixels[x];
					for (int y = _height - 1; y >= 0; --y) {
						const jjPALCOLOR src = column[y];
						if (src != transparent) {
							maps[0][x,y] = __NearestColors[src.red];
							maps[1][x,y] = __NearestColors[src.green];
							maps[2][x,y] = __NearestColors[src.blue];
						} else if (swingingVine)
							maps[0][x,y] = maps[1][x,y] = maps[2][x,y] = 128;
					}
				}
			} else { //32-bit
				const int alphaDivisor = halfOpacity ? 2 : 1;
				for (int x = _width - 1; x >= 0; --x) {
					const auto@ column = pixels[x];
					for (int y = _height - 1; y >= 0; --y) {
						const AlphaColor@ src = column[y];
						if (src.alpha == 255 && !halfOpacity) { //fully opaque
							maps[0][x,y] = __NearestColors[src.red];
							maps[1][x,y] = __NearestColors[src.green];
							maps[2][x,y] = __NearestColors[src.blue];
							maps[3][x,y] = 255 / alphaDivisor;
						} else if (src.alpha != 0) { //semi-opaque
							const uint8 reducedAlpha = src.alpha / alphaDivisor;
							maps[0][x,y] = __NearestColors[src.red * reducedAlpha / 255];
							maps[1][x,y] = __NearestColors[src.green * reducedAlpha / 255];
							maps[2][x,y] = __NearestColors[src.blue * reducedAlpha / 255];
							maps[3][x,y] = reducedAlpha;
						} else if (swingingVine)
							maps[0][x,y] = maps[1][x,y] = maps[2][x,y] = maps[3][x,y] = 128;
					}
				}
			}
			
			for (int subFrameID = 0; subFrameID < (!_hasAlpha ? 3 : 4); ++subFrameID)
				maps[subFrameID].save(jjAnimFrames[frameID + subFrameID]);
				
			jjAnimFrames[frameID].transparent = !_hasAlpha && halfOpacity; //only need to set the first one
			jjAnimFrames[frameID + 1].transparent = _hasAlpha;
		}
		void saveToAnimFrames(uint frameID, const Coordinates@ coordinates = null) const {
			if (coordinates is null || coordinates.width == 0 || coordinates.height == 0)
				@coordinates = @Coordinates(0, 0, width, height);
			Bitmap source(this, coordinates.left, coordinates.top, coordinates.width, coordinates.height);
			source.saveDirectlyToFrames(frameID, coordinates.transparent, coordinates.swingingVine);
			if (_caching)
				_savedBitmapsCache[(coordinates.transparent ? 'T' : 'F') + (coordinates.swingingVine ? 'T' : 'F') + frameID] = source;
			for (uint subFrameID = 0; subFrameID < NumberOfFramesPerImage; ++subFrameID) {
				jjANIMFRAME@ frame = jjAnimFrames[frameID + subFrameID];
				frame.hotSpotX = coordinates.hotSpotX;
				frame.hotSpotY = coordinates.hotSpotY;
				frame.gunSpotX = coordinates.gunSpotX;
				frame.gunSpotY = coordinates.gunSpotY;
				frame.coldSpotX= coordinates.coldSpotX;
				frame.coldSpotY= coordinates.coldSpotY;
			}
		}
		
		void swizzle(COLOR::Component red, COLOR::Component green, COLOR::Component blue) {
			for (int x = _width - 1; x >= 0; --x) {
				const auto@ column = pixels[x];
				for (int y = _height - 1; y >= 0; --y)
					column[y].swizzle(red, green, blue);
			}
		}
		
		void generateAlphaChannel() {
			if (!_hasAlpha) {
				_hasAlpha = true;
			}
		}
		void removeAlphaChannel(uint8 threshold = 127) {
			if (_hasAlpha) {
				_hasAlpha = false;
				const AlphaColor transparent;
				for (int x = _width - 1; x >= 0; --x) {
					const auto@ column = pixels[x];
					for (int y = _height - 1; y >= 0; --y)
						if (column[y].alpha < threshold)
							column[y] = transparent;
						//else
							//column[y].alpha = 255;
				}
			}
		}
	}
	
	class AlphaColor {
		protected jjPALCOLOR color;
		uint8 alpha;
		AlphaColor(){}
		AlphaColor@ opAssign(const jjPALCOLOR &in other) { color = other; alpha = (red != 0 || green != 0 || blue != 0) ? 255 : 0; return this; }
		AlphaColor@ opAssign(const AlphaColor &in other) { color = other.color; alpha = other.alpha; return this; }
		AlphaColor(uint8 r, uint8 g, uint8 b) { this = jjPALCOLOR(r,g,b); }
		AlphaColor(uint8 r, uint8 g, uint8 b, uint8 a) { color = jjPALCOLOR(r,g,b); alpha = a; }
		AlphaColor(const jjPALCOLOR &in other) { this = other; }
		AlphaColor(const AlphaColor &in other) { this = other; }
		bool opEquals (const jjPALCOLOR &in other) const { return color == other; }
		bool opEquals (const AlphaColor &in other) const { return color == other.color && alpha == other.alpha; }
		jjPALCOLOR opImplCast() const { return color; }
		
		uint8 red {
			get const { return color.red; }
			set { color.red = value; }
		};
		uint8 green {
			get const { return color.green; }
			set { color.green = value; }
		};
		uint8 blue {
			get const { return color.blue; }
			set { color.blue = value; }
		};
		
		uint8 getHue() const { return color.getHue(); }
		uint8 getSat() const { return color.getSat(); }
		uint8 getLight() const { return color.getLight(); }
		void setHSL(uint8 h, uint8 s, uint8 l) { color.setHSL(h,s,l); }
		void swizzle(COLOR::Component red, COLOR::Component green, COLOR::Component blue) { color.swizzle(red, green, blue); }
		
		jjPALCOLOR toPALCOLOR() const { return jjPALCOLOR(red, green, blue); }
	}
	
	class Coordinates {
		uint left = 0, right = 0, width = 0, top = 0, bottom = 0, height = 0;
		int hotSpotX = 0, hotSpotY = 0, gunSpotX = 0, gunSpotY = 0, coldSpotX = 0, coldSpotY = 0;
		bool transparent = false, swingingVine = false;
		Coordinates(){}
		Coordinates(uint l, uint t, uint w, uint h, int hX = 0, int hY = 0, int gX = 0, int gY = 0, int cX = 0, int cY = 0, bool tr = false, bool sv = false) {
			left = l; top = t;
			width = w; height = h;
			right = l+w; bottom = t+h;
			hotSpotX = hX; hotSpotY = hY;
			gunSpotX = gX; gunSpotY = gY;
			coldSpotX = cX; coldSpotY = cY;
			transparent = tr;
			swingingVine = sv;
		}
	}
	
	void AllocateSpriteSheet(const ANIM::Set setID, const Bitmap& bitmap, const array<array<Coordinates>>& setCoordinates = array<array<Coordinates>>(1, array<Coordinates>(1))) {
		array<uint> animSizes(setCoordinates.length);
		for (uint i = 0; i < animSizes.length; ++i) {
			animSizes[i] = setCoordinates[i].length * NumberOfFramesPerImage;
		}
		jjAnimSets[setID].allocate(animSizes);
		for (uint animID = 0; animID < animSizes.length; ++animID) {
			const array<Coordinates>@ animCoordinates = @setCoordinates[animID];
			for (uint frameID = 0; frameID < animCoordinates.length; ++frameID)
				bitmap.saveToAnimFrames(
					jjAnimations[jjAnimSets[setID].firstAnim + animID].firstFrame + frameID * NumberOfFramesPerImage,
					animCoordinates[frameID]
				);
		}
	}
	
	uint __getCurFrame(int setID, uint animation, uint frame) { //same code JJ2+ uses for converting to *FromCurFrame functions
		const jjANIMATION@ anim = jjAnimations[jjAnimSets[setID].firstAnim + animation];
		return anim.firstFrame + (frame % anim.frameCount);
	}
	void DrawSpriteFromCurFrame(float xPixel, float yPixel, uint sprite, int direction = 0, uint8 layerZ = 4, uint8 layerXY = 4, int8 playerID = -1) {
		if (!jjAnimFrames[sprite].transparent) {
			if (!jjAnimFrames[sprite+1].transparent)
				jjDrawSpriteFromCurFrame(xPixel, yPixel, sprite, direction, SPRITE::SINGLECOLOR, 0, layerZ, layerXY, playerID);
			else
				jjDrawSpriteFromCurFrame(xPixel, yPixel, sprite+3, direction, SPRITE::ALPHAMAP, 0, layerZ, layerXY, playerID);
			jjDrawSpriteFromCurFrame(xPixel, yPixel, sprite, direction, SPRITE::NEONGLOW, 0, layerZ, layerXY, playerID);
			jjDrawSpriteFromCurFrame(xPixel, yPixel, sprite+1, direction, SPRITE::NEONGLOW, 1, layerZ, layerXY, playerID);
			jjDrawSpriteFromCurFrame(xPixel, yPixel, sprite+2, direction, SPRITE::NEONGLOW, 2, layerZ, layerXY, playerID);
		} else
			jjDrawSpriteFromCurFrame(xPixel, yPixel, sprite, direction, SPRITE::SHADOW, 0, layerZ, layerXY, playerID);
		jjDrawSpriteFromCurFrame(xPixel, yPixel, sprite, direction, SPRITE::NEONGLOW, 0, layerZ, layerXY, playerID);
		jjDrawSpriteFromCurFrame(xPixel, yPixel, sprite+1, direction, SPRITE::NEONGLOW, 1, layerZ, layerXY, playerID);
		jjDrawSpriteFromCurFrame(xPixel, yPixel, sprite+2, direction, SPRITE::NEONGLOW, 2, layerZ, layerXY, playerID);
	}
	void DrawSprite(float xPixel, float yPixel, int setID, uint8 animation, uint frame, int direction = 0, uint8 layerZ = 4, uint8 layerXY = 4, int8 playerID = -1) {
		DrawSpriteFromCurFrame(xPixel, yPixel, __getCurFrame(setID, animation, frame), direction, layerZ, layerXY, playerID);
	}
	
	void DrawCanvasSpriteFromCurFrame(jjCANVAS@ canvas, int xPixel, int yPixel, uint sprite, int8 direction = 0) {
		if (!jjAnimFrames[sprite].transparent) {
			if (!jjAnimFrames[sprite+1].transparent)
				canvas.drawSpriteFromCurFrame(xPixel, yPixel, sprite, direction, SPRITE::SINGLECOLOR, 0);
			else
				canvas.drawSpriteFromCurFrame(xPixel, yPixel, sprite+3, direction, SPRITE::ALPHAMAP, 0);
			canvas.drawSpriteFromCurFrame(xPixel, yPixel, sprite, direction, SPRITE::NEONGLOW, 0);
			canvas.drawSpriteFromCurFrame(xPixel, yPixel, sprite+1, direction, SPRITE::NEONGLOW, 1);
			canvas.drawSpriteFromCurFrame(xPixel, yPixel, sprite+2, direction, SPRITE::NEONGLOW, 2);
		} else
			canvas.drawSpriteFromCurFrame(xPixel, yPixel, sprite, direction, SPRITE::SHADOW, 0);
		canvas.drawSpriteFromCurFrame(xPixel, yPixel, sprite, direction, SPRITE::NEONGLOW, 0);
		canvas.drawSpriteFromCurFrame(xPixel, yPixel, sprite+1, direction, SPRITE::NEONGLOW, 1);
		canvas.drawSpriteFromCurFrame(xPixel, yPixel, sprite+2, direction, SPRITE::NEONGLOW, 2);
	}
	void DrawCanvasSprite(jjCANVAS@ canvas, int xPixel, int yPixel, int setID, uint8 animation, uint frame, int8 direction = 0) {
		DrawCanvasSpriteFromCurFrame(canvas, xPixel, yPixel, __getCurFrame(setID, animation, frame), direction);
	}
	
	void DrawResizedSpriteFromCurFrame(float xPixel, float yPixel, uint sprite, float xScale, float yScale, uint8 layerZ = 4, uint8 layerXY = 4, int8 playerID = -1) {
		if (!jjAnimFrames[sprite].transparent) {
			if (!jjAnimFrames[sprite+1].transparent)
				jjDrawResizedSpriteFromCurFrame(xPixel, yPixel, sprite, xScale, yScale, SPRITE::SINGLECOLOR, 0, layerZ, layerXY, playerID);
			else
				jjDrawResizedSpriteFromCurFrame(xPixel, yPixel, sprite+3, xScale, yScale, SPRITE::ALPHAMAP, 0, layerZ, layerXY, playerID);
			jjDrawResizedSpriteFromCurFrame(xPixel, yPixel, sprite, xScale, yScale, SPRITE::NEONGLOW, 0, layerZ, layerXY, playerID);
			jjDrawResizedSpriteFromCurFrame(xPixel, yPixel, sprite+1, xScale, yScale, SPRITE::NEONGLOW, 1, layerZ, layerXY, playerID);
			jjDrawResizedSpriteFromCurFrame(xPixel, yPixel, sprite+2, xScale, yScale, SPRITE::NEONGLOW, 2, layerZ, layerXY, playerID);
		} else
			jjDrawResizedSpriteFromCurFrame(xPixel, yPixel, sprite, xScale, yScale, SPRITE::SHADOW, 0, layerZ, layerXY, playerID);
		jjDrawResizedSpriteFromCurFrame(xPixel, yPixel, sprite, xScale, yScale, SPRITE::NEONGLOW, 0, layerZ, layerXY, playerID);
		jjDrawResizedSpriteFromCurFrame(xPixel, yPixel, sprite+1, xScale, yScale, SPRITE::NEONGLOW, 1, layerZ, layerXY, playerID);
		jjDrawResizedSpriteFromCurFrame(xPixel, yPixel, sprite+2, xScale, yScale, SPRITE::NEONGLOW, 2, layerZ, layerXY, playerID);
	}
	void DrawResizedSprite(float xPixel, float yPixel, int setID, uint8 animation, uint frame, float xScale, float yScale, uint8 layerZ = 4, uint8 layerXY = 4, int8 playerID = -1) {
		DrawResizedSpriteFromCurFrame(xPixel, yPixel, __getCurFrame(setID, animation, frame), xScale, yScale, layerZ, layerXY, playerID);
	}
	
	void DrawCanvasResizedSpriteFromCurFrame(jjCANVAS@ canvas, int xPixel, int yPixel, uint sprite, float xScale, float yScale) {
		if (!jjAnimFrames[sprite].transparent) {
			if (!jjAnimFrames[sprite+1].transparent)
				canvas.drawResizedSpriteFromCurFrame(xPixel, yPixel, sprite, xScale, yScale, SPRITE::SINGLECOLOR, 0);
			else
				canvas.drawResizedSpriteFromCurFrame(xPixel, yPixel, sprite+3, xScale, yScale, SPRITE::ALPHAMAP, 0);
			canvas.drawResizedSpriteFromCurFrame(xPixel, yPixel, sprite, xScale, yScale, SPRITE::NEONGLOW, 0);
			canvas.drawResizedSpriteFromCurFrame(xPixel, yPixel, sprite+1, xScale, yScale, SPRITE::NEONGLOW, 1);
			canvas.drawResizedSpriteFromCurFrame(xPixel, yPixel, sprite+2, xScale, yScale, SPRITE::NEONGLOW, 2);
		} else
			canvas.drawResizedSpriteFromCurFrame(xPixel, yPixel, sprite, xScale, yScale, SPRITE::SHADOW, 0);
		canvas.drawResizedSpriteFromCurFrame(xPixel, yPixel, sprite, xScale, yScale, SPRITE::NEONGLOW, 0);
		canvas.drawResizedSpriteFromCurFrame(xPixel, yPixel, sprite+1, xScale, yScale, SPRITE::NEONGLOW, 1);
		canvas.drawResizedSpriteFromCurFrame(xPixel, yPixel, sprite+2, xScale, yScale, SPRITE::NEONGLOW, 2);
	}
	void DrawCanvasResizedSprite(jjCANVAS@ canvas, int xPixel, int yPixel, int setID, uint8 animation, uint frame, float xScale, float yScale) {
		DrawCanvasResizedSpriteFromCurFrame(canvas, xPixel, yPixel, __getCurFrame(setID, animation, frame), xScale, yScale);
	}
	
	void DrawRotatedSpriteFromCurFrame(float xPixel, float yPixel, uint sprite, int angle, float xScale = 1, float yScale = 1, uint8 layerZ = 4, uint8 layerXY = 4, int8 playerID = -1) {
		if (!jjAnimFrames[sprite].transparent) {
			if (!jjAnimFrames[sprite+1].transparent)
				jjDrawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite, angle, xScale, yScale, SPRITE::SINGLECOLOR, 0, layerZ, layerXY, playerID);
			else
				jjDrawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite+3, angle, xScale, yScale, SPRITE::ALPHAMAP, 0, layerZ, layerXY, playerID);
			jjDrawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite, angle, xScale, yScale, SPRITE::NEONGLOW, 0, layerZ, layerXY, playerID);
			jjDrawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite+1, angle, xScale, yScale, SPRITE::NEONGLOW, 1, layerZ, layerXY, playerID);
			jjDrawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite+2, angle, xScale, yScale, SPRITE::NEONGLOW, 2, layerZ, layerXY, playerID);
		} else
			jjDrawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite, angle, xScale, yScale, SPRITE::SHADOW, 0, layerZ, layerXY, playerID);
		jjDrawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite, angle, xScale, yScale, SPRITE::NEONGLOW, 0, layerZ, layerXY, playerID);
		jjDrawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite+1, angle, xScale, yScale, SPRITE::NEONGLOW, 1, layerZ, layerXY, playerID);
		jjDrawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite+2, angle, xScale, yScale, SPRITE::NEONGLOW, 2, layerZ, layerXY, playerID);
	}
	void DrawRotatedSprite(float xPixel, float yPixel, int setID, uint8 animation, uint frame, int angle, float xScale = 1, float yScale = 1, uint8 layerZ = 4, uint8 layerXY = 4, int8 playerID = -1) {
		DrawRotatedSpriteFromCurFrame(xPixel, yPixel, __getCurFrame(setID, animation, frame), angle, xScale, yScale, layerZ, layerXY, playerID);
	}
	
	void DrawCanvasRotatedSpriteFromCurFrame(jjCANVAS@ canvas, int xPixel, int yPixel, uint sprite, int angle, float xScale = 1, float yScale = 1) {
		if (!jjAnimFrames[sprite].transparent) {
			if (!jjAnimFrames[sprite+1].transparent)
				canvas.drawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite, angle, xScale, yScale, SPRITE::SINGLECOLOR, 0);
			else
				canvas.drawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite+3, angle, xScale, yScale, SPRITE::ALPHAMAP, 0);
			canvas.drawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite, angle, xScale, yScale, SPRITE::NEONGLOW, 0);
			canvas.drawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite+1, angle, xScale, yScale, SPRITE::NEONGLOW, 1);
			canvas.drawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite+2, angle, xScale, yScale, SPRITE::NEONGLOW, 2);
		} else
			canvas.drawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite, angle, xScale, yScale, SPRITE::SHADOW, 0);
		canvas.drawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite, angle, xScale, yScale, SPRITE::NEONGLOW, 0);
		canvas.drawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite+1, angle, xScale, yScale, SPRITE::NEONGLOW, 1);
		canvas.drawRotatedSpriteFromCurFrame(xPixel, yPixel, sprite+2, angle, xScale, yScale, SPRITE::NEONGLOW, 2);
	}
	void DrawCanvasRotatedSprite(jjCANVAS@ canvas, int xPixel, int yPixel, int setID, uint8 animation, uint frame, int angle, float xScale = 1, float yScale = 1) {
		DrawCanvasRotatedSpriteFromCurFrame(canvas, xPixel, yPixel, __getCurFrame(setID, animation, frame), angle, xScale, yScale);
	}
	
	void DrawSwingingVineSpriteFromCurFrame(float xPixel, float yPixel, uint sprite, int length, int curvature, uint8 layerZ = 4, uint8 layerXY = 4, int8 playerID = -1) {
		if (!jjAnimFrames[sprite].transparent) {
			if (!jjAnimFrames[sprite+1].transparent)
				jjDrawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite, length, curvature, SPRITE::SINGLECOLOR, 0, layerZ, layerXY, playerID);
			else
				jjDrawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite+3, length, curvature, SPRITE::ALPHAMAP, 0, layerZ, layerXY, playerID);
			jjDrawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite, length, curvature, SPRITE::NEONGLOW, 0, layerZ, layerXY, playerID);
			jjDrawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite+1, length, curvature, SPRITE::NEONGLOW, 1, layerZ, layerXY, playerID);
			jjDrawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite+2, length, curvature, SPRITE::NEONGLOW, 2, layerZ, layerXY, playerID);
		} else
			jjDrawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite, length, curvature, SPRITE::SHADOW, 0, layerZ, layerXY, playerID);
		jjDrawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite, length, curvature, SPRITE::NEONGLOW, 0, layerZ, layerXY, playerID);
		jjDrawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite+1, length, curvature, SPRITE::NEONGLOW, 1, layerZ, layerXY, playerID);
		jjDrawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite+2, length, curvature, SPRITE::NEONGLOW, 2, layerZ, layerXY, playerID);
	}
	void DrawSwingingVineSprite(float xPixel, float yPixel, int setID, uint8 animation, uint frame, int length, int curvature, uint8 layerZ = 4, uint8 layerXY = 4, int8 playerID = -1) {
		DrawSwingingVineSpriteFromCurFrame(xPixel, yPixel, __getCurFrame(setID, animation, frame), length, curvature, layerZ, layerXY, playerID);
	}
	
	void DrawCanvasSwingingVineSpriteFromCurFrame(jjCANVAS@ canvas, int xPixel, int yPixel, uint sprite, int length, int curvature) {
		if (!jjAnimFrames[sprite].transparent) {
			if (!jjAnimFrames[sprite+1].transparent)
				canvas.drawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite, length, curvature, SPRITE::SINGLECOLOR, 0);
			else
				canvas.drawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite+3, length, curvature, SPRITE::ALPHAMAP, 0);
			canvas.drawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite, length, curvature, SPRITE::NEONGLOW, 0);
			canvas.drawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite+1, length, curvature, SPRITE::NEONGLOW, 1);
			canvas.drawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite+2, length, curvature, SPRITE::NEONGLOW, 2);
		} else
			canvas.drawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite, length, curvature, SPRITE::SHADOW, 0);
		canvas.drawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite, length, curvature, SPRITE::NEONGLOW, 0);
		canvas.drawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite+1, length, curvature, SPRITE::NEONGLOW, 1);
		canvas.drawSwingingVineSpriteFromCurFrame(xPixel, yPixel, sprite+2, length, curvature, SPRITE::NEONGLOW, 2);
	}
	void DrawCanvasSwingingVineSprite(jjCANVAS@ canvas, int xPixel, int yPixel, int setID, uint8 animation, uint frame, int length, int curvature) {
		DrawCanvasSwingingVineSpriteFromCurFrame(canvas, xPixel, yPixel, __getCurFrame(setID, animation, frame), length, curvature);
	}
	
	void DrawObject(jjOBJ@ obj) {
		if (obj.freeze != 0 || obj.justHit != 0)
			obj.draw();
		else
			DrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction);
	}
}