Downloads containing CrystalEmpireVex.j2as

Downloads
Name Author Game Mode Rating
JJ2 1.23 vanilla: Miscellaneous stuff Violet CLM Multiple N/A Download file

File preview

enum CustomAnimSets {
	caCLOUD, caMOUNTAINS, caHILLS, caPOPULATEDHILLS, caGEMSTONE, caGLOW, caEND
};
array<jjANIMSET@> ANIMSETS(caEND, null);
array<uint> FirstAnimFrames(caEND, 0);

void AssembleAnimSet(CustomAnimSets ca, uint frameCount) {
	@ANIMSETS[ca] = jjAnimSets[ANIM::CUSTOM[ca]].allocate(array<uint>={frameCount});
	FirstAnimFrames[ca] = jjAnimations[ANIMSETS[ca].firstAnim].firstFrame; //global value
	
}

void onLevelLoad() {
	AssembleAnimSet(caCLOUD, 1);
	jjANIMFRAME@ frame = jjAnimFrames[FirstAnimFrames[caCLOUD]];
	{
		jjPIXELMAP cloudMap(0, 0, 7*32, 4*32, 8);
		for (uint y = cloudMap.height; y-- != 0; ) { //make background transparent
			for (uint x = 0; x < 112; ++x)
				if (cloudMap[x,y] == 180) cloudMap[x,y] = 0;
				else break;
			for (uint x = cloudMap.width - 1; x > 112; --x)
				if (cloudMap[x,y] == 180) cloudMap[x,y] = 0;
				else break;
		}
		cloudMap.save(frame);
	}
	frame.hotSpotX = -frame.width / 2; frame.hotSpotY = -frame.height / 2;
	
	AssembleAnimSet(caMOUNTAINS, 4);
	for (uint i = 0; i < 4; ++i) {
		@frame = jjAnimFrames[FirstAnimFrames[caMOUNTAINS] + i];
		jjPIXELMAP mountainMap(0, (64 + i*4)*32, i<3 ? 8*32 : 9*32, 4*32, 5);
		mountainMap.save(frame);
		frame.hotSpotX = -frame.width / 2;
		frame.hotSpotY = 0;
	}
	
	AssembleAnimSet(caHILLS, 4);
	for (uint i = 0; i < 4; ++i) {
		@frame = jjAnimFrames[FirstAnimFrames[caHILLS] + i];
		if (i < 2) {
			jjPIXELMAP hillMap(8*32, (64 + i*4)*32, 11*32, 4*32, 5);
			hillMap.save(frame);
		} else {
			jjPIXELMAP towerMap((7+i)*32, (72)*32, (i-1)*32, 6*32, 5);
			jjPIXELMAP resizedTowerMap(towerMap.width >> 2, towerMap.height >> 2);
			for (uint xx = 0; xx < resizedTowerMap.width; ++xx)
				for (uint yy = 0; yy < resizedTowerMap.height; ++yy)
					resizedTowerMap[xx,yy] = towerMap[xx<<2, yy<<2];
			resizedTowerMap.save(frame);
		}
		frame.hotSpotX = -frame.width / 2;
		frame.hotSpotY = 0;
	}
	
	jjSetDarknessColor(jjPALCOLOR(255,255,255));
	
	jjGenerateSettableTileArea(3, 0, 0, jjLayerWidth[3], jjLayerHeight[3]);
	
	jjIsSnowing = true;
	jjIsSnowingOutdoorsOnly = true;
	jjSnowingType = SNOWING::FLOWER;
	
	jjObjectPresets[OBJECT::STEADYLIGHT].behavior = LensFlare;
	jjObjectPresets[OBJECT::STEADYLIGHT].curAnim = jjObjectPresets[OBJECT::SUPERGEM].curAnim;
	jjObjectPresets[OBJECT::STEADYLIGHT].lightType = LIGHT::BRIGHT;
	jjObjectPresets[OBJECT::STEADYLIGHT].var[0] = jjObjectPresets[OBJECT::BLUEGEM].var[0];
	
	
	AssembleAnimSet(caGEMSTONE, 1);
	{
		@frame = jjAnimFrames[FirstAnimFrames[caGEMSTONE]];
		jjPIXELMAP gemMap(9*32, 78*32, 1*32, 2*32, 5);
		gemMap.save(frame);
	}
	frame.hotSpotX = -frame.width / 2;
	frame.hotSpotY = -frame.height / 2;
	jjObjectPresets[OBJECT::RECTBLUEGEM].behavior = Gemstone;
	jjObjectPresets[OBJECT::RECTBLUEGEM].curFrame = FirstAnimFrames[caGEMSTONE];
	jjObjectPresets[OBJECT::RECTBLUEGEM].playerHandling = HANDLING::PARTICLE;
}


array<jjOBJ@> FlickerLights(jjLocalPlayerCount, null);
void MakeLightingFlicker(jjPLAYER@ play) {
	jjOBJ@ light = @FlickerLights[play.localPlayerID];
	if (light is null || light.objectID == 0 || light.eventID != OBJECT::STEADYLIGHT) {
		@light = @FlickerLights[play.localPlayerID] = jjObjects[jjAddObject(OBJECT::STEADYLIGHT, 0, 0, 0, CREATOR::OBJECT, BEHAVIOR::BEES)];
		light.light = 127;
		light.lightType = LIGHT::FLICKER;
	} else {
		light.xPos = play.cameraX + jjSubscreenWidth/2;
		light.yPos = play.cameraY + jjSubscreenHeight/2;
	}
	
	play.lighting = int8(jjSin(jjRenderFrame) * 25) + 77;
	
	if (CURRENTPALETTE == palEVIL && !jjLowDetail) {
		if ((jjRenderFrame & 31) == 31) {
			if ((jjRenderFrame & 63) == 31)
				jjSetDarknessColor(jjPALCOLOR(0,0,0));
			else
				jjSetDarknessColor(jjPALCOLOR(128,0,128));
		}
	}
	
}
void SetupFlickeringLighting() {
	for (int i = 0; i < jjLocalPlayerCount; ++i)
		jjLocalPlayers[i].lightType = LIGHT::NONE;
}


class Cloud {
	float xPos, yPos, scale;
	Cloud() {
		xPos = jjSubscreenWidth + 128;
		yPos = jjRandom() % (jjSubscreenHeight/2);
		scale = (jjRandom() & 0b11110000) / 256.0f;
	}
	void draw(jjCANVAS@ screen) {
		screen.drawResizedSpriteFromCurFrame(int(xPos), int(yPos), FirstAnimFrames[caCLOUD], scale, scale, SPRITE::TRANSLUCENT);
	}
};
array<Cloud> Clouds;
void AddClouds() {
	if ((jjRandom() & 31) == 0)
		Clouds.insertLast(Cloud());
}
void DrawClouds(jjCANVAS@ screen) {
	for (uint i = 0; i < Clouds.length; ++i) {
		Cloud@ cloud = @Clouds[i];
		cloud.xPos -= cloud.scale * 4; //clouds are moved in an onDraw, meaning their speed is tied to jjRenderFrame, not jjGameTicks
		if (cloud.xPos < -128) {
			Clouds.removeAt(i); --i;
		} else {
			cloud.draw(screen);
		}
	}
}


const float MOUNTAINSPEED = 0.12f;
const int MOUNTAINYOFFSET = 200;
const float MOUNTAINYFLOATDIVIDE = float(LEVELHEIGHT*50);
class Mountain {
	int xPos, xPosTileWidth, yPos;
	float yFloat;
	uint curFrame;
	Mountain(){}
	Mountain(int x, int y) {
		xPos = x; xPosTileWidth = x + 800; yPos = y + MOUNTAINYOFFSET; yFloat = y / MOUNTAINYFLOATDIVIDE;
		curFrame = FirstAnimFrames[caMOUNTAINS] + (jjRandom() & 3);
	}
	Mountain(int x, int y, uint frameID) { //for hills
		xPos = x; xPosTileWidth = x + 800; yPos = y + HILLYOFFSET; yFloat = y / HILLYFLOATDIVIDE;
		//if (frameID < POPULATEDHILLCOUNT)
			curFrame = FirstAnimFrames[caPOPULATEDHILLS] + (frameID % POPULATEDHILLCOUNT);
		//else
		//	curFrame = FirstAnimFrames[caHILLS] + (jjRandom() & 1);
		
	}
	void draw(int xOffset, int yOffset, float cameraY, jjCANVAS@ screen) {
		const int yDraw = yPos - yOffset - int(yFloat * cameraY);
		screen.drawSpriteFromCurFrame(xPos - xOffset, yDraw, curFrame);
		if (xPos < 200) //do a Tile Width effect
			screen.drawSpriteFromCurFrame(xPosTileWidth - xOffset, yDraw, curFrame);
	}
};
array<Mountain> Mountains, Hills;
void AddMountains() { //done only once
	const array<array<int>> mountainPositions = {
		{ 18,  40},
		{175,  22},
		{375,  40},
		{570,  13},
		{740,  45},
		{ 75,  85},
		{270,  55},
		{450,  92},
		{637,  75},
		{ 85, 130},
		{360, 130},
		{195,  95},
		{540,  90},
		{745, 140},
		{ 95, 165},
		{ 20, 150},
		{315, 150},
		{670, 165},
		{570, 185},
		{185, 195},
		{420, 170},
		{485, 150}
	};
	for (uint i = mountainPositions.length; i-- != 0; ) {
		const array<int>@ positions = @mountainPositions[i];
		Mountains.insertLast(Mountain(positions[0], positions[1]));
	}
}
void DrawMountains(jjPLAYER@ play, jjCANVAS@ screen) {
	const int xOffset = int(play.cameraX * MOUNTAINSPEED);
	const int yOffset = int((480 - jjSubscreenHeight)/2 + play.cameraY * MOUNTAINSPEED/2);
	for (uint i = Mountains.length; i-- != 0; )
		Mountains[i].draw(xOffset, yOffset, play.cameraY, screen);
}

const float HILLSPEED = 0.175f;
const int HILLYOFFSET = 336;
const float HILLYFLOATDIVIDE = float(LEVELHEIGHT*47);
const uint POPULATEDHILLCOUNT = 10;
const uint HILLTOPEMPTYSPACE = 24;
void AddHills() {
	AssembleAnimSet(caPOPULATEDHILLS, POPULATEDHILLCOUNT);
	
	jjPIXELMAP firstHill(jjAnimFrames[FirstAnimFrames[caHILLS]+0]);
	jjPIXELMAP secondHill(jjAnimFrames[FirstAnimFrames[caHILLS]+1]);
	const array<jjPIXELMAP@> hills = {@firstHill, @secondHill};
	
	const jjPIXELMAP firstBuilding(jjAnimFrames[FirstAnimFrames[caHILLS]+2]);
	const jjPIXELMAP secondBuilding(jjAnimFrames[FirstAnimFrames[caHILLS]+3]);
	const array<const jjPIXELMAP@> buildings = {@firstBuilding, @secondBuilding};
	
	for (uint i = 0; i < POPULATEDHILLCOUNT; ++i) {
		jjPIXELMAP populatedHill(firstHill.width, firstHill.height + HILLTOPEMPTYSPACE);
		
		for (uint j = 15 + (jjRandom() & 7); j-- != 0; ) { //add buildings
			const jjPIXELMAP@ sourceBuilding = @buildings[j&1];
			const uint xDraw = (jjRandom()%320); //position the building on the hill
			const uint yDraw = 96 + (jjRandom() & 15) - int(jjSin(int(xDraw*1.6f))*96);
			//jjAlert("" + xDraw +": " + yDraw);
			const int hueShift = (int(jjRandom() & 3) - 8) * 8; //buildings in four different colors
			for (uint x = 0; x < sourceBuilding.width; ++x)
				for (uint y = 0; y < sourceBuilding.height; ++y) {
					if (yDraw + y >= populatedHill.height)
						break;
					if (sourceBuilding[x,y] != 0)
						populatedHill[5 + xDraw + x, yDraw + y] = sourceBuilding[x,y] + hueShift;
				}
		}
		
		for (uint x = 0; x < firstHill.width; ++x)
			for (uint y = 0; y < firstHill.height; ++y) {
				uint8 color = hills[i&1][x,y];
				if (color != 0) { //draw the hill in front of the building
					if ((i&3)>1) color += 2;
					populatedHill[x,y+HILLTOPEMPTYSPACE] = color;
				}
			}
		
		jjANIMFRAME@ frame = jjAnimFrames[FirstAnimFrames[caPOPULATEDHILLS] + i];
		populatedHill.save(frame);
		frame.hotSpotX = -frame.width / 2;
	}
	/*for (uint i = 0; i < 2; ++i) { //now that the initial hill sprites have been harvested for the populated hills, recolor the originals to be a bit darker
		for (uint x = 0; x < firstHill.width; ++x)
			for (uint y = 0; y < firstHill.height; ++y) {
				const uint8 color = hills[i][x,y];
				if (color != 0)
					hills[i][x,y] = color + 2; //palshift to dark
			}
		hills[i].save(jjAnimFrames[FirstAnimFrames[caHILLS]+i]);
	}*/
	const array<array<int>> hillPositions = {
		{525,   5},
		{ 82,  17},
		{290,  55},
		{ 50,  80},
		{480,  85},
		{630, 110},
		{150, 145},
		{280, 160},
		{630, 175},
		{435, 185},
		{  0, 190},
		{195, 225},
		{525, 250},
		{ 40, 275},
		{350, 290},
		{560, 315},
		{155, 320}
	};
	for (uint i = hillPositions.length; i-- != 0; ) {
		const array<int>@ positions = @hillPositions[i];
		Hills.insertLast(Mountain(positions[0], positions[1], i));
	}
}
void DrawHills(jjPLAYER@ play, jjCANVAS@ screen) {
	const int xOffset = int(play.cameraX * HILLSPEED);
	const int yOffset = int((480 - jjSubscreenHeight)/2 + play.cameraY * HILLSPEED/2);
	for (uint i = Hills.length; i-- != 0; ) {
		Hills[i].draw(xOffset, yOffset, play.cameraY, screen);
	}
}


const uint FIRSTFLOORTILE = 1;
const uint LASTFLOORTILE = 15;
const uint FIRSTWALLTILE = LASTFLOORTILE + 1;
const uint LASTWALLTILE = 39;
const uint FIRSTWALLSLOPE = 20;
array<array<bool>> TilesAreFloor(LEVELWIDTH, array<bool>(LEVELHEIGHT, false));
void DetermineWhichTilesAreFloor() {
	for (uint xTile = 0; xTile < LEVELWIDTH; ++xTile)
		for (uint yTile = 0; yTile < LEVELHEIGHT; ++yTile) {
			const uint16 tileID = jjTileGet(5, xTile,yTile) & TILE::RAWRANGE;
			TilesAreFloor[xTile][yTile] = tileID >= FIRSTFLOORTILE && tileID <= LASTFLOORTILE;
		}
}
bool objectInView(int xPos, int yPos) {
	for (int i = 0; i < jjLocalPlayerCount; ++i) {
		jjPLAYER@ play = jjLocalPlayers[i];
		if (xPos > play.cameraX - 64 && xPos < play.cameraX + jjSubscreenWidth + 64 && yPos > play.cameraY - 64 && yPos < play.cameraY + jjSubscreenHeight + 64)
			return true;
	}
	return false;
}
void DrawReflection(int xPos, int yPos, int direction, uint curFrame, SPRITE::Mode spriteMode, uint8 spriteParam) {
	if (!objectInView(xPos, yPos)) return;
	const uint xTile = xPos >> 5;
	if (xTile < 0 || xTile >= TilesAreFloor.length) return;
	const uint yTile = yPos >> 5;
	if (yTile < 0 || yTile + 1 >= TilesAreFloor[0].length) return;
	if (TilesAreFloor[xTile][yTile] || TilesAreFloor[xTile][yTile+1]) {
		const int distance = jjMaskedTopVLine(xPos, yPos, 70);
		if (distance <= 70) //within range
			jjDrawSpriteFromCurFrame(xPos, yPos + distance*2, curFrame, direction ^ 0x40, spriteMode, spriteParam);
	}
}
void DrawReflections() {
	for (uint i = 0; i < 32; ++i) {
		jjPLAYER@ play = jjPlayers[i];
		if (play.isInGame && play.blink == 0 && play.spriteMode == SPRITE::PLAYER)
			DrawReflection(int(play.xPos), int(play.yPos), play.direction, play.curFrame, SPRITE::TRANSLUCENTPLAYER, i);
	}
	for (uint i = jjObjectCount - 1; i > 0; --i) {
		jjOBJ@ obj = jjObjects[i];
		if (obj.isActive && obj.curFrame != 0 && obj.behavior != Gemstone && obj.behavior != BEHAVIOR::ELECTROBULLET)
			DrawReflection(int(obj.xPos), int(obj.yPos), obj.direction, obj.curFrame, SPRITE::TRANSLUCENT, 0);
	}
}


uint TILESETSIZE = 150; //first (unused) tileID to be replaced by a blurred wall
bool LASTTILEFLIPPED = false;
array<jjPIXELMAP@>@ BlendTileRow(uint yTile) {
	array<jjPIXELMAP@> row;
	return @row;
}
const uint LEVELWIDTH = jjLayerWidth[4];
const uint LEVELHEIGHT = jjLayerHeight[4];
const uint YTILEMAX = LEVELHEIGHT - 1;
const uint GENERICFLOORTILE = 9;
const array<int> YOFFSETS = {0, -1, 1};
array<array<uint16>>@ BlendRow(uint yTile) {
	array<array<uint16>> results;
	for (uint xTile = 0; xTile < LEVELWIDTH; ++xTile) {
		array<uint16> blendablePositions;
		for (uint layerID = 5; layerID >= 3; --layerID) { //layers with 1,1 speeds
			const uint16 tileID = jjTileGet(layerID, xTile, yTile);
			const uint16 tileIDRAW = tileID & TILE::RAWRANGE;
			if (tileIDRAW >= FIRSTWALLTILE && tileIDRAW <= LASTWALLTILE) {
				blendablePositions.insertLast(tileID);
				if (layerID == 4 && tileIDRAW != 0 && !blendablePositions.isEmpty() && blendablePositions[0] == GENERICFLOORTILE)
					blendablePositions.insertAt(0, tileID);
			} else if (tileIDRAW >= FIRSTFLOORTILE && tileIDRAW <= LASTFLOORTILE)
				blendablePositions.insertLast(GENERICFLOORTILE);
		}
		results.insertLast(blendablePositions);
	}
	return results;
}
jjPIXELMAP@ MakePixelMap(array<uint16>@ blendablePositionsOther) {
	if (blendablePositionsOther.isEmpty())
		return null;/*
	bool isWallNotFloor = false;
	for (uint i = 0; i < blendablePositionsOther.length; ++i)
		if (blendablePositionsOther[i] != GENERICFLOORTILE) {
			isWallNotFloor = true;
			break;
		}
	if (!isWallNotFloor)
		return null;*/
	
	jjPIXELMAP blendedTile(blendablePositionsOther[0]);
	for (uint additionalTileIndex = 1; additionalTileIndex < blendablePositionsOther.length; ++additionalTileIndex) {
		jjPIXELMAP additionalTile(blendablePositionsOther[additionalTileIndex]);
		for (uint xx = 0; xx < additionalTile.width; ++xx)
			for (uint yy = 0; yy < additionalTile.height; ++yy)
				if (additionalTile[xx,yy] != 0)
					blendedTile[xx,yy] = additionalTile[xx,yy];
	}
	return @blendedTile;
}
uint GetColor(int xx, int yy, const array<array<jjPIXELMAP@>>& tileImages) {
	int xTile = 1, yTile = 1;
	if (xx < 0) { xx += 32; xTile -= 1;}
	else if (xx >= 32) { xx -= 32; xTile += 1;}
	if (yy < 0) { yy += 32; yTile -= 1;}
	else if (yy >= 32) { yy -= 32; yTile += 1;}
	jjPIXELMAP@ map = tileImages[yTile][xTile];
	if (map is null) return 0;
	return map[xx,yy];
}

array<array<jjPIXELMAP@>@> normalBlendedTiles;
array<array<array<uint16>>@> blendableTileIDs;
void BlurWalls() {
	if ((jjGameTicks & 15) != 15)
		return;
	const uint yTile = jjGameTicks >> 4; //for ease of reference
	if (yTile == 0) {
		normalBlendedTiles.insertLast(@array<jjPIXELMAP@>(LEVELWIDTH, null));
		normalBlendedTiles.insertLast(@normalBlendedTiles[0]);
		blendableTileIDs.insertLast(@BlendRow(0));
		blendableTileIDs.insertLast(@blendableTileIDs[0]);
	} else if (yTile >= LEVELHEIGHT) {
		if (yTile == LEVELHEIGHT) {
			normalBlendedTiles.resize(0);
			blendableTileIDs.resize(0);
		}
		return;
	}
	
	if (yTile < YTILEMAX) {
		normalBlendedTiles.insertLast(array<jjPIXELMAP@>(LEVELWIDTH, null));
		blendableTileIDs.insertLast(BlendRow(yTile + 1));
	} else {
		normalBlendedTiles.insertLast(@normalBlendedTiles[1]);
		blendableTileIDs.insertLast(@blendableTileIDs[1]);
	}
	
	for (uint xTile = 0; xTile < LEVELWIDTH; ++xTile) {
		//jjAlert("" + xTile + ", " + yTile);
		array<uint16>@ blendablePositionsLocal = @blendableTileIDs[1][xTile];
		if (blendablePositionsLocal.length > 0) { //Blurable
			bool isWallNotFloor = false;
			for (uint i = 0; i < blendablePositionsLocal.length; ++i)
				if (blendablePositionsLocal[i] != GENERICFLOORTILE) {
					isWallNotFloor = true;
					break;
				}
			if (!isWallNotFloor) {
				//jjAlert("floor at " + xTile + ", " + yTile);
				continue;
			}
			//jjAlert("wall  at " + xTile + ", " + yTile);
			
			for (int xOffset = -1; xOffset <= 1; ++xOffset)
				for (uint yOffset = 0; yOffset < 3; ++yOffset) {
					//if (xOffset != 0 && yOffset != 0) //diagonal
					//	continue;
					int xToTest = xTile + xOffset;
					if (xToTest < 0) xToTest = 0;
					else if (xToTest >= int(LEVELWIDTH)) xToTest = LEVELWIDTH - 1;
					const uint yToTest = 1 + YOFFSETS[yOffset];
					if (normalBlendedTiles[yToTest][xToTest] is null) { //no pixel map here yet
						@normalBlendedTiles[yToTest][xToTest] = MakePixelMap(@blendableTileIDs[yToTest][xToTest]);
					}
				}
			
			{
				int xTileLeft = xTile - 1;
				if (xTileLeft < 0) xTileLeft = 0;
				int xTileRight = xTile + 1;
				if (xTileRight >= int(LEVELWIDTH)) xTileRight = LEVELWIDTH - 1;
				const bool stackHeight = blendableTileIDs[1][xTile].length > 1;
				const uint oldTileID = blendableTileIDs[1][xTile][0];
				jjPIXELMAP result(oldTileID);
				const array<array<jjPIXELMAP@>> tileImages = {
					{@normalBlendedTiles[0][xTileLeft], @normalBlendedTiles[0][xTile], @normalBlendedTiles[0][xTileRight]},
					{@normalBlendedTiles[1][xTileLeft], @normalBlendedTiles[1][xTile], @normalBlendedTiles[1][xTileRight]},
					{@normalBlendedTiles[2][xTileLeft], @normalBlendedTiles[2][xTile], @normalBlendedTiles[2][xTileRight]},
				};
				const jjPIXELMAP@ tileLocal = @tileImages[1][1];
				bool differentFromBaseTile = false;
				for (uint xx = 0; xx < result.width; ++xx)
					for (uint yy = 0; yy < result.height; ++yy) {
						const uint originalColor = result[xx,yy];
						if (originalColor != 0) {
							const uint basicColor = tileLocal[xx,yy];
							if (!stackHeight && xx > 3 && xx < 28 && yy > 3 && yy < 28) //inside pixel of a single-tile stack
								result[xx,yy] = basicColor;
							else {
								uint averagePixel = 0;
								
								uint i = 0;
								for (int xOffset = -4; xOffset <= 4; xOffset += 2)
									for (int yOffset = -4; yOffset <= 4; yOffset += 2) {
										if (abs(xOffset) + abs(yOffset) != 4) //corners
											continue;
										const uint colorToAdd = GetColor(xx + xOffset, yy + yOffset, tileImages);
										averagePixel += (colorToAdd != 0) ? colorToAdd : tileLocal[xx,yy];
									}
								
								averagePixel >>= 3; //mean
								if (averagePixel != originalColor) {
									//if (averagePixel != basicColor)
									//	if ((jjRandom() & 1) == 1)
									//		averagePixel -= 1; //noise
									differentFromBaseTile = true;
								}
								result[xx,yy] = averagePixel;
							}
						}
					}
				if (differentFromBaseTile) {
					//jjAlert("" +TILESETSIZE);
					if (LASTTILEFLIPPED) {
						result.save(TILESETSIZE | TILE::HFLIPPED);
						jjTileSet(3, xTile, yTile, TILESETSIZE++ | TILE::HFLIPPED);
						LASTTILEFLIPPED = false;
					} else {
						result.save(TILESETSIZE);
						jjTileSet(3, xTile, yTile, TILESETSIZE);
						LASTTILEFLIPPED = true;
					}
				} else
					jjTileSet(3, xTile, yTile, oldTileID);
			}
		}
		
	}
	
	normalBlendedTiles.removeAt(0);
	blendableTileIDs.removeAt(0);
}


const uint GLOWSIZE = 6;
const uint GLOWFADEMAX = 127;
const uint GLOWFADEOFF = 20;
const uint8 GLOWCOLORINDEX = 214;
const array<uint16> GlowingRawTileIDs = {55, 64, 66, 67, 68, 74, 75, 76, 85};
class Glow {
	float xPos, yPos, xScale, yScale;
	uint layer, curFrame;
	Glow(){}
	Glow(uint x, uint y, uint l, int frameID, bool h, bool v) {
		curFrame = FirstAnimFrames[caGLOW] + frameID;
		const jjANIMFRAME@ frame = jjAnimFrames[curFrame];
		xPos = x * 32; yPos = y * 32; layer = l;
		if (h) {
			xScale = -1;
			xPos += 32 - frame.coldSpotX;
		} else {
			xScale = 1;
			xPos += frame.coldSpotX;
		}
		if (v) {
			yScale = -1;
			yPos += 32 - frame.coldSpotY;
		} else {
			yScale = 1;
			yPos += frame.coldSpotY;
		}
	}
	void draw(float scale) {
		jjDrawResizedSpriteFromCurFrame(xPos, yPos, curFrame, scale * xScale, scale * yScale, SPRITE::ALPHAMAP, 15, layer);
	}
};
array<Glow> Glows;
void DetermineWhichTilesGlow() {
	AssembleAnimSet(caGLOW, GlowingRawTileIDs.length);
	for (uint tileIndex = 0; tileIndex < GlowingRawTileIDs.length; ++tileIndex) {
		const jjPIXELMAP tileImage(GlowingRawTileIDs[tileIndex]);
		uint LEFT = 32;
		uint TOP = 32;
		uint RIGHT = 0;
		uint BOTTOM = 0;
		for (uint x = 0; x < 32; ++x)
			for (uint y = 0; y < 32; ++y)
				if (tileImage[x,y] == GLOWCOLORINDEX) {
					if (LEFT > x) LEFT = x;
					if (TOP > y) TOP = y;
					if (RIGHT < x) RIGHT = x;
					if (BOTTOM < y) BOTTOM = y;
				}
		const uint width = RIGHT+1-LEFT;
		const uint height = BOTTOM+1-TOP;
		jjPIXELMAP glow(width + GLOWSIZE*2, height + GLOWSIZE*2);
		for (uint x = 0; x < width; ++x) {
			bool seenColorInColumn = false;
			for (uint y = 0; y <= height; ++y) { //<= because this needs to hit a transparent pixel beneath the bottommost opaque pixel
				if (tileImage[LEFT+x,TOP+y] == GLOWCOLORINDEX) {
					glow[GLOWSIZE+x,GLOWSIZE+y] = GLOWFADEMAX;
					if (!seenColorInColumn) {
						seenColorInColumn = true;
						for (uint i = 1; i <= GLOWSIZE; ++i)
							glow[GLOWSIZE+x,GLOWSIZE+y-i] = GLOWFADEMAX - i*GLOWFADEOFF;
					}
				} else if (seenColorInColumn) {
					seenColorInColumn = false;
					for (uint i = 0; i < GLOWSIZE; ++i)
						glow[GLOWSIZE+x,GLOWSIZE+y+i] = GLOWFADEMAX - i*GLOWFADEOFF;
				}
			}
		}
		for (uint y = 0; y < height; ++y) {
			bool seenColorInRow = false;
			for (uint x = 0; x <= width; ++x) {
				if (tileImage[LEFT+x,TOP+y] == GLOWCOLORINDEX) {
					if (!seenColorInRow) {
						seenColorInRow = true;
						for (uint i = 1; i <= GLOWSIZE; ++i) {
							const uint8 newIntensity = GLOWFADEMAX - i*GLOWFADEOFF;
							const uint8 oldIntensity = glow[GLOWSIZE+x-i,GLOWSIZE+y];
							if (newIntensity > oldIntensity) //maybe better to add them somehow, but I'll settle for this
								glow[GLOWSIZE+x-i,GLOWSIZE+y] = newIntensity;
						}
					}
				} else if (seenColorInRow) {
					seenColorInRow = false;
					for (uint i = 0; i < GLOWSIZE; ++i) {
						const uint8 newIntensity = GLOWFADEMAX - i*GLOWFADEOFF;
						const uint8 oldIntensity = glow[GLOWSIZE+x+i,GLOWSIZE+y];
						if (newIntensity > oldIntensity)
							glow[GLOWSIZE+x+i,GLOWSIZE+y] = newIntensity;
					}
				}
			}
		}
		jjANIMFRAME@ frame = jjAnimFrames[FirstAnimFrames[caGLOW] + tileIndex];
		glow.save(jjAnimFrames[FirstAnimFrames[caGLOW] + tileIndex]);
		frame.hotSpotX = -frame.width/2;
		frame.hotSpotY = -frame.height/2;
		frame.coldSpotX = LEFT-GLOWSIZE-frame.hotSpotX;
		frame.coldSpotY = TOP-GLOWSIZE-frame.hotSpotY;
	}
	
	for (uint xTile = 0; xTile < LEVELWIDTH; ++xTile)
		for (uint yTile = 0; yTile < LEVELHEIGHT; ++yTile)
			for (uint layer = 5; layer >= 2; --layer) {
				const uint16 rawTileID = jjTileGet(layer, xTile, yTile);
				const uint16 baseTileID = rawTileID & TILE::RAWRANGE;
				const int arrayIndex = GlowingRawTileIDs.find(baseTileID);
				if (arrayIndex > -1) {
					Glows.insertLast(Glow(xTile, yTile, layer, arrayIndex, (rawTileID & TILE::HFLIPPED) != 0, (rawTileID & TILE::VFLIPPED) != 0));
				}
			}
}

void DrawGlows() {
	const float scale = jjSin(jjGameTicks << 3) / 2 + 1.5f;
	for (uint i = Glows.length; i-- != 0; )
		Glows[i].draw(scale);
}


void ReadyFlare() {
	if (jjAnimSets[ANIM::FLARE] == 0)
		jjAnimSets[ANIM::FLARE].load();
	for (uint i = 0; i < 6; ++i) {
		jjANIMFRAME@ frame = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::FLARE]]+i];
		jjPIXELMAP circle(frame);
		uint borderSize = i+2; if (i >= 3) borderSize -= 1;
		for (uint x = borderSize; x < circle.width-borderSize; ++x) {
			uint drawStage = 0;
			for (uint y = 0; y < circle.height; ++y) { //fill in FLARE circles
				const uint8 color = circle[x,y];
				if (drawStage == 0) {
					if (color == 16)
						drawStage = 1;
				} else if (drawStage == 1) {
					if (color == 0) {
						drawStage = 2;
						circle[x,y] = 16;
					}
				} else if (drawStage == 2) {
					if (color == 0)
						circle[x,y] = 16;
					else break;
				}
			}
		}
		circle.save(frame);
	}
}

const array<float> LensFlareDeltas = {0.80f, 0.33f, 1.75f, 1.25f, 2.00f};
void LensFlare(jjOBJ@ obj) {
	if (CURRENTPALETTE == palEVIL) {
		obj.light = 0;
		return;
	}
	if (obj.state == STATE::START) {
		obj.state = STATE::WAKE;
		obj.xPos -= 15; //center
	}
	obj.light = 12;
	obj.frameID = (jjGameTicks >> 4) % jjAnimations[obj.curAnim]; //rotate slowly
	obj.determineCurFrame();
	obj.draw(); //will automatically be drawn as a gem
	
	//the flare is low-intensity enough that I'm comfortable not checking jjLowDetail for it
	
	const float xPos = obj.xPos, yPos = obj.yPos;
	const float halfWidth = jjSubscreenWidth/2.f;
	const float halfHeight = jjSubscreenHeight/2.f;
	for (int localPlayerID = 0; localPlayerID < jjLocalPlayerCount; ++localPlayerID) {
		const jjPLAYER@ play = jjLocalPlayers[localPlayerID];
		const float xPositionOnScreen = xPos - play.cameraX;
		if (xPositionOnScreen <= -32 || xPositionOnScreen >= jjSubscreenWidth + 32)
			continue;
		const float yPositionOnScreen = yPos - play.cameraY;
		if (yPositionOnScreen <= -32 || yPositionOnScreen >= jjSubscreenHeight + 32)
			continue;
		const float xDelta = halfWidth - xPositionOnScreen;
		const float yDelta = halfHeight - yPositionOnScreen;
		const int8 playerID = play.playerID;
		for (uint i = 0; i < 5; ++i)
			jjDrawSprite(xPos + xDelta*LensFlareDeltas[i], yPos + yDelta*LensFlareDeltas[i], ANIM::FLARE, 0, 1+i, 1, SPRITE::TRANSLUCENTCOLOR, 15, 1, 4, playerID);
	}
}


void Gemstone(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.state = STATE::WAKE;
		const int xTile = int(obj.xOrg/32);
		const int yTile = int(obj.yOrg/32);
		obj.age = (jjParameterGet(xTile, yTile, 0, 1) == 1) ? 2 : 4; //layer
		obj.special = jjParameterGet(xTile, yTile, 1, 2) << 3; //palshift
		obj.xAcc = (jjParameterGet(xTile, yTile, 3, 2) + 1) * 0.25f; //size
		obj.animSpeed = jjParameterGet(xTile, yTile, 5, 4) * 64; //angle
	}
	jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.animSpeed, obj.xAcc, obj.xAcc, SPRITE::PALSHIFT, obj.special, obj.age);
}


void RecolorSnow() {
	for (uint i = 0; i < 1024; ++i) {
		jjPARTICLE@ part = jjParticles[i];
		if (part.type == PARTICLE::FLOWER)
			switch(CURRENTPALETTE) {
				case palEVIL:
					part.flower.color = 0; //black
					break;
				case palNIGHT:
					break;
				case palEVENING:
					break;
				case palDAY: default:
					part.flower.color = 15; //white
					break;
			}
	}
}


enum PaletteOptions {
	palDAY, palEVENING, palNIGHT, palEVIL
}
int8 CURRENTPALETTE = palDAY;
void onFunction0(jjPLAYER@ play, int8 paletteIndex) {
	if (CURRENTPALETTE == paletteIndex)
		return; //nothing to change, don't bother
	if (paletteIndex != palDAY)
		jjPalette.load("CrystalEmpireV" + (paletteIndex+1) + ".pal");
	switch (CURRENTPALETTE = paletteIndex) {
		case palEVIL:
			jjSetDarknessColor(jjPALCOLOR(0,0,0));
			break;
		case palNIGHT:
			break;
		case palEVENING:
			break;
		case palDAY: default:
			jjPalette.reset();
			jjSetDarknessColor(jjPALCOLOR(255,255,255));
			break;
	}
	jjPalette.apply();
	jjSetFadeColors();
}


void onLevelBegin() {
	AddMountains();
	AddHills();
	SetupFlickeringLighting();
	DetermineWhichTilesAreFloor();
	DetermineWhichTilesGlow();
	ReadyFlare();
}
void onMain() {
	BlurWalls(); //spread out over the first LEVELHEIGHT gameticks*n
	RecolorSnow();
	if (!jjLowDetail) {
		AddClouds();
		DrawReflections();
		DrawGlows();
	}
}
void onDrawLayer4(jjPLAYER@ play, jjCANVAS@ screen) { //called even while spectating
	MakeLightingFlicker(play);
}
void onDrawLayer8(jjPLAYER@ play, jjCANVAS@ screen) { //Layer 8 is always at position 0,0, so these are screen-absolute coordinates
	if (!jjLowDetail) {
		DrawClouds(screen);
		DrawMountains(play, screen);
		DrawHills(play, screen);
		//screen.drawRectangle(0, jjSubscreenHeight - 60, jjSubscreenWidth, 10, jjPalette.findNearestColor(jjPALCOLOR(90,111,130)));
		//screen.drawRectangle(0, jjSubscreenHeight - 50, jjSubscreenWidth, 50, jjPalette.findNearestColor(jjPALCOLOR(194,202,230)));
	}
}

//TEMP!
bool onDrawScore(jjPLAYER@ play, jjCANVAS@ screen) { return true; }
bool onDrawHealth(jjPLAYER@ play, jjCANVAS@ screen) { return true; }
bool onDrawAmmo(jjPLAYER@ play, jjCANVAS@ screen) { return true; }
bool onDrawLives(jjPLAYER@ play, jjCANVAS@ screen) { return true; }