Downloads containing BossrushV4.j2as

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

File preview

const array<uint16> GroundTilesToRecolor = {292, 293, 395, 405}; //tiles used by the textured floor that need to fit into the proper palette space
const array<uint16> SewerTilesToRecolor = {247, 257, 267, 306, 307, 316, 317, 326, 327};
jjPIXELMAP@ SkyTexture, SurfaceGroundTexture, SewerTexture, SewerGroundTexture, TopTexture, BottomTexture;
jjPLAYER@ Play;
jjOBJ@ PlayObject;
const float FLOORBASICXOFFSET = 96; //middle of screen
const float PLAYERYSTART = 161*32; //where in the event map the player proxy object is created (presumably should be at/near the bottom of the level)
const float CAMERAMINDISTANCE = 18.f; //arbitrary constant, could even be 0
float CameraY = PLAYERYSTART + CAMERAMINDISTANCE;
uint ShadowFrame;
const array<int> HotSpotY3DCharAdjustments = {6, 7, 11, 12, 14, 6, 6};
uint TimeSpentInSewer = 0;
float SewerGroundScrollOffset = 0;
jjPAL DayPalette, NightPalette;
void onLevelLoad() {
	jjChat("/3Ddepth 0");
	jjChat("/3D TAB");
	jjAnimSets[ANIM::JAZZ3D].load();
	jjAnimSets[ANIM::SPAZ3D].load();
	jjCharacters[CHAR::JAZZ].airJump = AIR::NONE; //This is just me feeling that the air jump moves don't add much to this 3D gameplay. They could be reinstated simply by removing these lines.
	jjCharacters[CHAR::SPAZ].airJump = AIR::NONE;
	if (jjIsTSF) jjCharacters[CHAR::LORI].airJump = AIR::NONE;
	
	jjWeapons[WEAPON::BLASTER].defaultSample = false;
	jjObjectPresets[OBJECT::BLASTERBULLET].behavior = BEHAVIOR::INACTIVE; //I'm replacing shooting with rolling. If you do want to include some sort of bullet mechanic, this should be replaced with a custom behavior that sets up a bullet in the 3D object space.
	
	for (uint i = 0; i < HotSpotY3DCharAdjustments.length; ++i) {
		jjANIMATION@ anim = jjAnimations[jjAnimSets[ANIM::JAZZ3D] + i];
		jjANIMATION@ anim2 = jjAnimations[jjAnimSets[ANIM::SPAZ3D] + i];
		for (uint j = 0; j < anim.frameCount; ++j)
			jjAnimFrames[anim + j].hotSpotY -= HotSpotY3DCharAdjustments[i]; //align 3D char frames with FEET, not whatever odd places the original team decided to put those hotspots
		for (uint j = 0; j < anim2.frameCount; ++j)
			jjAnimFrames[anim2 + j].hotSpotY -= HotSpotY3DCharAdjustments[i];
	}
	
	ShadowFrame = jjAnimations[jjAnimSets[ANIM::PINBALL].load()]; //it's a circle; what more can you ask for?
	
	jjUseLayer8Speeds = true;
	jjLayerXOffset[8] = FLOORBASICXOFFSET;
	generateMode7SpritePositionLookupTable();
	
	DayPalette = jjBackupPalette;
	DayPalette.copyFrom(144, 16, 112, DayPalette); //get the wall gradient into the latter half of the palette space, which is what textured backgrounds can draw
	DayPalette.apply();
	
	NightPalette.load("Colon2.j2t");
	NightPalette.copyFrom(144, 16, 112, NightPalette); //yes you too
	NightPalette.copyFrom(96 | 128, 8, 96, NightPalette); //same for the sewer
	NightPalette.gradient(113,247,148, 18,88,58, 176, 32);
	
	for (uint i = 0; i < GroundTilesToRecolor.length; ++i) { //
		jjPIXELMAP tile(GroundTilesToRecolor[i]);
		for (uint x = 0; x < 32; ++x)
			for (uint y = 0; y < 32; ++y) {
				uint8 color = tile[x,y];
				if (color >= 112 && color <= 127)
					tile[x,y] = color + 32;
			}
		tile.save(GroundTilesToRecolor[i], true);
	}
	for (uint i = 0; i < SewerTilesToRecolor.length; ++i) {
		jjPIXELMAP tile(SewerTilesToRecolor[i]);
		for (uint x = 0; x < 32; ++x)
			for (uint y = 0; y < 32; ++y)
				tile[x,y] |= 128;
		tile.save(SewerTilesToRecolor[i], true);
	} { //recolor shingles to SurfaceGroundTexture
		jjPIXELMAP tile(899);
		for (uint x = 0; x < 32; ++x)
			for (uint y = 0; y < 32; ++y)
				tile[x,y] += 8;
		tile.save(241 | TILE::VFLIPPED, true);
		tile.save(899, true);
	}
	
	@TopTexture = @SkyTexture = jjPIXELMAP(TEXTURE::NORMAL);
	@BottomTexture = @SurfaceGroundTexture = jjPIXELMAP(0, 512, 256, 256, 4);
	@SewerTexture = jjPIXELMAP(0, 0, 256, 256, 8);
	@SewerGroundTexture = jjPIXELMAP(0, 768, 256, 256, 4);
	for (uint x = 0; x < 256; ++x)
		for (uint y = 0; y < 256; ++y)
			if (SewerGroundTexture[x,y] == 0)
				SewerGroundTexture[x,y] = SkyTexture[x,y];
	
	@Play = jjLocalPlayers[0];
	@PlayObject = jjObjects[jjAddObject(OBJECT::STEADYLIGHT, 320, PLAYERYSTART, Play.playerID, CREATOR::PLAYER, BEHAVIOR::PlayerProxy)];
	PlayObject.deactivates = false; //draw 3D
	
	jjObjectPresets[OBJECT::REDGEM].behavior = MyPickup;
	jjObjectPresets[OBJECT::REDGEM].scriptedCollisions = true; //won't use the standard onObjectHit, since nothing ever comes into contact with the actual player object, but this can be used for equivalent purposes
	//jjObjectPresets[OBJECT::REDGEM].curAnim = jjObjectPresets[OBJECT::SUPERGEM].curAnim;
	jjANIMATION@ gemAnim = jjAnimations[jjObjectPresets[OBJECT::REDGEM].curAnim];
	for (uint i = 0; i < gemAnim.frameCount; ++i)
		jjAnimFrames[gemAnim.firstFrame + i].hotSpotY = -1 - int(jjAnimFrames[gemAnim.firstFrame + i].height); //all 3D objects should have hotspots near the bottoms of their frames, specifically where they come into contact with the SurfaceGroundTexture if yPos==0
	
	jjObjectPresets[OBJECT::BUBBLER].behavior = MyBubble;
	--jjObjectPresets[OBJECT::BUBBLER].curAnim;
	jjObjectPresets[OBJECT::BUBBLER].var[0] = 9;
	
	//the following assignments deal specifically with Colon and would naturally need editing (or outright removing) for other tilesets
	jjObjectPresets[OBJECT::STEADYLIGHT].behavior = UnmovingEyecandySprite;
	jjObjectPresets[OBJECT::STEADYLIGHT].curAnim = jjAnimSets[ANIM::CUSTOM[0]].allocate(array<uint> = {5});
	jjPIXELMAP streetlamp(8*32, 512, 32, 4*32, 4);
	for (uint x = 0; x < 32; ++x)
		for (uint y = 114; y < 128; ++y)
			if (streetlamp[x,y] >= 136)
				streetlamp[x,y] = 0;
	jjANIMFRAME@ frame = jjAnimFrames[jjObjectPresets[OBJECT::STEADYLIGHT].determineCurFrame()];
	streetlamp.save(frame);
	frame.hotSpotX = -16;
	frame.hotSpotY = -120;
	
	jjObjectPresets[OBJECT::ONEUPBARREL].behavior = UnmovingEyecandySprite;
	jjObjectPresets[OBJECT::ONEUPBARREL].curFrame = jjObjectPresets[OBJECT::STEADYLIGHT].curFrame + 1;
	jjPIXELMAP barrel(9*32, 512, 32, 51, 4);
	for (uint x = 0; x < 32; ++x)
		for (uint y = 0; y < 51; ++y) {
			uint8 color = barrel[x,y];
			if (color<= 127)
				barrel[x,y] = 0;
			else if (color >= 136 && color <= 142 && (y > 42 || x < 2 || x > 43))
				barrel[x,y] = 0;
		}
	@frame = jjAnimFrames[jjObjectPresets[OBJECT::ONEUPBARREL].determineCurFrame()];
	barrel.save(frame);
	frame.hotSpotX = -16;
	frame.hotSpotY = -50;
	
	jjObjectPresets[OBJECT::ONEUPCRATE].behavior = UnmovingEyecandySprite;
	jjObjectPresets[OBJECT::ONEUPCRATE].curFrame = jjObjectPresets[OBJECT::STEADYLIGHT].curFrame + 2;
	jjPIXELMAP bench(10*32, 512 + 21, 64, 38, 4);
	@frame = jjAnimFrames[jjObjectPresets[OBJECT::ONEUPCRATE].determineCurFrame()];
	bench.save(frame);
	frame.hotSpotX = -32;
	frame.hotSpotY = -37;
	
	jjObjectPresets[OBJECT::BIGBOX].behavior = UnmovingEyecandySprite;
	jjObjectPresets[OBJECT::BIGBOX].curFrame = jjObjectPresets[OBJECT::STEADYLIGHT].curFrame + 3;
	jjPIXELMAP house(53*32,0, 21*32, 15*32, 4);
	for (uint x = 0; x < house.width; ++x)
		for (uint y = 14*32; y < 15*32; ++y)
			if (house[x,y] >= 136) house[x,y] = 0; //remove floor
	@frame = jjAnimFrames[jjObjectPresets[OBJECT::BIGBOX].determineCurFrame()];
	house.save(frame);
	frame.hotSpotX = -frame.width/2;
	frame.hotSpotY = -frame.height;
	
	jjObjectPresets[OBJECT::STOMPSCENERY].behavior = ManholeCover;
	jjObjectPresets[OBJECT::STOMPSCENERY].curFrame = jjObjectPresets[OBJECT::STEADYLIGHT].curFrame + 4;
	jjPIXELMAP manhole(10*32 + 2, 512 + 2*32 + 6, 61, 16, 4);
	@frame = jjAnimFrames[jjObjectPresets[OBJECT::STOMPSCENERY].determineCurFrame()];
	manhole.save(frame);
	frame.hotSpotX = -30;
	frame.hotSpotY = -8;
}
void onLevelBegin() {
	for (int i = jjObjectCount - 1; i >= 1; --i) {
		jjOBJ@ obj = jjObjects[i];
		if (obj.isActive && obj.deactivates && obj.behavior != BEHAVIOR::TRIGGERSCENERY) //not 3D
			obj.delete();
	}
	for (int y = jjLayerHeight[4] - 1; y > 22; --y) //22 is the bottom of the Jazz area
		for (int x = 0; x < jjLayerWidth[4]; ++x) {
			uint eventID = jjEventGet(x, y);
			if (eventID != 0) {
				jjOBJ@ obj = jjObjects[jjAddObject(eventID, x * 32, y * 32, 0, CREATOR::LEVEL)];
				obj.deactivates = false; //draw 3D, and don't get erased by the repointed camera
			}
		}
	Play.cameraFreeze(0, 0, false, true);
}

	
array<float> VerticalPositionsForLinesInTexturedGround();
void generateMode7SpritePositionLookupTable() {
	for (float i = 0; i < 260; ++i)
		VerticalPositionsForLinesInTexturedGround.insertLast((0x3C0000 / (i + 8) * i / 2048.f));
}

bool DrawingToLowerSubscreen(jjPLAYER@ play) { return play.subscreenY > 0; }
bool onDrawScore(jjPLAYER@ play, jjCANVAS@ screen) { return true || DrawingToLowerSubscreen(play); } //remove "true ||" if you want Score included
bool onDrawHealth(jjPLAYER@ play, jjCANVAS@ screen) { return DrawingToLowerSubscreen(play); }
bool onDrawAmmo(jjPLAYER@ play, jjCANVAS@ screen) { return true || !DrawingToLowerSubscreen(play); } //and again
bool onDrawLives(jjPLAYER@ play, jjCANVAS@ screen) { return jjDifficulty <= 0 || !DrawingToLowerSubscreen(play); }

bool onCheat(string &in cheat) {
	if (cheat == "jjmorph") { //don't allow characters without 3D sprites
		if (Play.charCurr == CHAR::JAZZ)
			Play.morphTo(CHAR::SPAZ);
		else
			Play.morphTo(CHAR::JAZZ);
		return true;
	}
	return false;
}

float max(float a, float b) {
	if (a > b) return a;
	return b;
}
float min(float a, float b) {
	if (a < b) return a;
	return b;
}
void onPlayerInput(jjPLAYER@ play) {
	play.keySelect = play.keyDown = false;
	play.idle = 0;
	if (PlayObject.special > 0) { //heading toward manhole
		jjOBJ@ manhole = jjObjects[PlayObject.special];
		if (play.xPos > manhole.xPos + 7.5) play.keyLeft = true;
		else if (play.xPos < manhole.xPos - 7.5) play.keyRight = true;
		else play.keyLeft = play.keyRight = false;
		play.keyUp = (PlayObject.yOrg > manhole.yOrg + 4);
		play.keyJump = false;
	}
}
const float MAXDISTANCEFROMCOLLISIONOBJECT = 20;
void onMain() {
	//if (PlayObject.special <= 0) { //not moved by another object
		if (PlayObject.special <= 0 && Play.ballTime <= 0 && Play.keyFire && (Play.ySpeed == 0 || !Play.alreadyDoubleJumped)) { //roll!
			Play.ballTime = 70;
			Play.ySpeed = -5.5;
			PlayObject.ySpeed = -6;
			Play.alreadyDoubleJumped = true;
			jjSamplePriority(SOUND::AMMO_BULFL2);
		} else if (Play.ballTime <= 0 && Play.keyUp) {
			if (Play.keyRun)
				PlayObject.ySpeed = max(PlayObject.ySpeed - 0.07f, -3.f);
			else
				PlayObject.ySpeed = max(PlayObject.ySpeed - 0.045f, -2.f);
			
		} else { //assumes, as with the camera code below, that the player will never move backwards; adjust as needed if that is not the case.
			PlayObject.ySpeed = min(PlayObject.ySpeed + 0.08f, 0.f);
		}
		PlayObject.xPos = Play.xPos;
		PlayObject.yOrg = PlayObject.yOrg + PlayObject.ySpeed;
		if (TimeSpentInSewer != 1)
			PlayObject.yPos = Play.yPos - Play.yOrg - 12.5f; //distance from SurfaceGroundTexture
		else
			PlayObject.yPos = jjObjects[PlayObject.special].yPos; //descending on manhole
	//}
	
	//CameraY = PlayObject.yOrg;
	if (CameraY > PlayObject.yOrg + CAMERAMINDISTANCE)
		CameraY -= (CameraY - (PlayObject.yOrg + CAMERAMINDISTANCE)) / ((Play.ballTime <= 20) ? 10 : 25);
	
	for (int i = jjObjectCount - 1; i >= 1; --i) {
		jjOBJ@ obj = jjObjects[i];
		if (!obj.deactivates && obj.behavior != PlayerProxy && obj.behavior != ManholeCover)
			obj.xPos = obj.xOrg - 4 * SewerGroundScrollOffset;
		if (obj.isActive && obj.scriptedCollisions)
			if ( //this is conceptually and computationally simple but I'm not 100% thrilled with it yet...
				abs(obj.xPos - PlayObject.xPos) <= MAXDISTANCEFROMCOLLISIONOBJECT &&
				abs(obj.yPos - PlayObject.yPos) <= MAXDISTANCEFROMCOLLISIONOBJECT && 
				abs(obj.yOrg - PlayObject.yOrg) <= MAXDISTANCEFROMCOLLISIONOBJECT
			) { //collide!
				obj.state = STATE::ACTION;
			}
	}
	
	if (TimeSpentInSewer > 1) {
		TimeSpentInSewer += 1;
		SewerGroundScrollOffset = jjSin(TimeSpentInSewer) * 256;
		if (PlayObject.yOrg < MANHOLESTARTY - 7 || abs(PlayObject.xPos - MANHOLESTARTX) > 64) //not standing on the manhole cover
			if (PlayObject.yPos >= -1 && int(PlayObject.xPos - 12 + 4 * SewerGroundScrollOffset) / 4 & 255 > 128) //standing in sludge
				Play.hurt();
	}
}

array<SPRITE::Mode> ObjectSpriteModes = {SPRITE::NORMAL, SPRITE::GEM, SPRITE::GEM, SPRITE::GEM, SPRITE::GEM, SPRITE::NORMAL, SPRITE::NORMAL, SPRITE::NORMAL, SPRITE::NORMAL, SPRITE::TRANSLUCENTPALSHIFT};
bool drawObject(jjOBJ@ obj, jjCANVAS@ screen, float yPos) {
	//xPos: xPos
	//yPos: yPos
	//yOrg: zPos
	//yAcc: offset of yPos (used by pickups)
	if (!obj.isActive || obj.deactivates || obj.curFrame == 0 || obj.yOrg < MANHOLESTARTY - 32 && TimeSpentInSewer == 0) return false;
	
	float distanceFromFade = VerticalPositionsForLinesInTexturedGround[jjSubscreenHeight] - (CameraY - obj.yOrg);
	if (distanceFromFade <= 1300)
		return false;
	if (distanceFromFade > 1900) {
		obj.delete();
		return false;
	}
	for (uint j = 0; true; ++j) {
		if (j >= VerticalPositionsForLinesInTexturedGround.length) return false;
		if (VerticalPositionsForLinesInTexturedGround[j] > distanceFromFade) {
			distanceFromFade = float(j) / float(jjSubscreenHeight);
			break;
		}
	}
		
	float distanceFromCamera = 1.f - distanceFromFade;
	
	float xPos = jjSubscreenWidth/2 - distanceFromFade * (jjSubscreenWidth/2 - (obj.xPos - 964)) * 1.0714285714f; //this doesn't work quite right at other resolutions yet
	//if (obj.behavior != PlayerProxy && obj.behavior != ManholeCover) //those two don't scroll back and forth with the rest of the sewer
	//	xPos -= distanceFromFade * 4 * SewerGroundScrollOffset;
	yPos -= distanceFromCamera * float(jjSubscreenHeight);
	if (obj.behavior != UnmovingEyecandySprite && obj.behavior != MyBubble) //always stuck to the SurfaceGroundTexture, no shadow needed
		screen.drawResizedSpriteFromCurFrame(int(xPos), int(yPos), ShadowFrame, distanceFromFade, distanceFromFade / 2, SPRITE::SHADOW);
	screen.drawResizedSpriteFromCurFrame(int(xPos), int(yPos + (obj.yPos + obj.yAcc) * distanceFromFade), obj.curFrame, distanceFromFade * 2, distanceFromFade * 2, ObjectSpriteModes[obj.var[0]], obj.var[0] - 1);
	//screen.drawPixel(xPos, yPos, 15); //debug: show exact position
	return true;
}
void onDrawLayer8(jjPLAYER@ play, jjCANVAS@ screen) {
	if (DrawingToLowerSubscreen(play)) {
		jjTexturedBGFadePositionY = 1; //sky
		jjLayerYOffset[8] = -CameraY;
		if (TimeSpentInSewer > 0) {
			//jjTexturedBGStyle = TEXTURE::TUNNEL;
			//jjLayerXOffset[8] = 0;
		} else jjLayerXAutoSpeed[8] = 0.75;
		TopTexture.makeTexture();
		jjLayerYOffset[2] = Layer2YOffset;
	} else {
		jjTexturedBGFadePositionY = 0; //SurfaceGroundTexture
		jjLayerYOffset[8] = CameraY;
		if (TimeSpentInSewer > 0) {
			//jjTexturedBGStyle = TEXTURE::WARPHORIZON;
			jjLayerXOffset[8] = FLOORBASICXOFFSET + SewerGroundScrollOffset;
		} else jjLayerXAutoSpeed[8] = 0;
		BottomTexture.makeTexture();
		jjLayerYOffset[2] = Layer2YOffset + jjSubscreenHeight;
	}
}
void onDrawLayer4(jjPLAYER@ play, jjCANVAS@ screen) {
	for (int i = jjObjectCount - 1; i >= 1; --i) {
		drawObject(jjObjects[i], screen, jjResolutionHeight - play.subscreenY);
	}
}

//0: idle
//1: jump
//2: roll
//3: run
//4: skid
//5: spin
//6: stand
void PlayerProxy(jjOBJ@ obj) {
	int16 oldCurAnim = obj.curAnim;
	ANIM::Set animSet = (Play.charCurr == CHAR::SPAZ) ? ANIM::SPAZ3D : ANIM::JAZZ3D; //sorry, Lori
	if (PlayObject.special > 0 && jjObjects[PlayObject.special].state == STATE::TURN) //standing on the manhole cover
		obj.determineCurAnim(animSet, 5); //spin
	else if (Play.ballTime > 20)
		obj.determineCurAnim(animSet, 2); //roll
	else if (Play.ySpeed != 0)
		obj.determineCurAnim(animSet, 1); //jump
	else if ((Play.xSpeed != 0 || obj.ySpeed != 0) && (Play.keyLeft || Play.keyRight || Play.keyUp))
		obj.determineCurAnim(animSet, 3); //run
	else if (Play.xSpeed != 0 || obj.ySpeed != 0)
		obj.determineCurAnim(animSet, 4); //skid
	else
		obj.determineCurAnim(animSet, 0); //idle
	if (obj.curAnim != oldCurAnim) {
		obj.frameID = 0;
		obj.determineCurFrame();
	} else if (jjGameTicks & 3 == 3) {
		++obj.frameID;
		obj.determineCurFrame();
	}
	if (Play.blink != 0 && jjGameTicks & 15 < 8)
		obj.curFrame = 0; //don't draw
}

void MyPickup(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.yPos = -32 * jjParameterGet(uint(obj.xOrg / 32), uint(obj.yOrg / 32), 0, 3);
		obj.state = STATE::IDLE;
	} if (obj.state == STATE::ACTION) {
		obj.state = STATE::KILL;
		obj.curAnim = obj.killAnim;
		obj.frameID = 0;
		obj.determineCurFrame();
		obj.scriptedCollisions = false;
		jjSamplePriority(SOUND::COMMON_PICKUP1);
		//potentially do pickup-related stuff here: score, ammo, whatever.
	}
	obj.yAcc = 4 * jjSin(int((jjGameTicks + obj.objectID * 8 + obj.xOrg + obj.yPos * 256) * 16)); //standard formula for determining a pickup's yPos offset
	if (jjGameTicks & 3 == 3) {
		if (uint(++obj.frameID) >= jjAnimations[obj.curAnim].frameCount) {
			if (obj.state == STATE::KILL) {
				obj.delete();
			} else
				obj.frameID = 0;
		}
		obj.determineCurFrame();
	}
}
void UnmovingEyecandySprite(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.yPos = 0;
		obj.yAcc = 0;
		obj.state = STATE::IDLE;
	}
}
float MANHOLESTARTX;
float MANHOLESTARTY;
float Layer2YOffset = 0;
void ManholeCover(jjOBJ@ obj) {
	switch (obj.state) {
	case STATE::START:
		obj.yPos = 0;
		obj.yAcc = 0;
		obj.state = STATE::DELAYEDSTART;
		MANHOLESTARTX = obj.xOrg;
		MANHOLESTARTY = obj.yOrg;
	case STATE::DELAYEDSTART:
		if (PlayObject.special == 0) {
			if (PlayObject.yOrg - MANHOLESTARTY < 256) {
				PlayObject.special = obj.objectID; //approach manhole
				obj.counter = 0;
			}
		} else if (PlayObject.special == obj.objectID) {
			if (PlayObject.yOrg < obj.yOrg) {
				PlayObject.yOrg = obj.yOrg; //don't overshoot
				PlayObject.ySpeed = 0;
			}
			if (Play.xSpeed == 0 && PlayObject.ySpeed == 0 && ++obj.counter == 70) {
				obj.state = STATE::TURN;
				obj.counter = 0;
			}
		}
		break;
	case STATE::TURN:
		obj.counter += 2;
		if (obj.yPos < 0) obj.yPos = obj.yPos + 2;
		Layer2YOffset = obj.counter * 2;
		if (obj.counter == 220)
			@BottomTexture = @SewerGroundTexture;
		else if (obj.counter > 220 && obj.counter <= 320 && (obj.counter & 3) == 2) {
			jjPalette = DayPalette;
			jjPalette.copyFrom(1, 254, 1, NightPalette, (obj.counter - 220) / 100.f);
			jjPalette.apply();
			if (obj.counter == 270)
				obj.yPos = -760;
		} else if (obj.counter == 350) {
			@TopTexture = @SewerTexture;
			TimeSpentInSewer = 1;
			jjLayerXAutoSpeed[8] = 0;
			jjTriggers[0] = true;
			jjSetFadeColors(0);
			MANHOLESTARTY -= 64;
			CameraY -= 64;
			obj.yOrg = MANHOLESTARTY;
			PlayObject.yOrg = MANHOLESTARTY;
		} else if (obj.counter == 1140) {
			jjSamplePriority(SOUND::JAZZSOUNDS_PFOE);
			PlayObject.special = 0;
			TimeSpentInSewer = 2;
			obj.state = STATE::DONE;
		}
		break;
	}
}
void MyBubble(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		obj.yPos = 0;
		obj.yAcc = 0;
		obj.state = STATE::DELAYEDSTART;
	}
	if (TimeSpentInSewer == 0) return;
	uint rand = jjRandom();
	if (obj.state == STATE::DELAYEDSTART) {
		obj.curFrame = 0;
		if (rand & 15 == 0) obj.state = STATE::FLY;
	} else if (obj.state == STATE::FLY) {
		if (jjGameTicks & 3 == 0)
			++obj.frameID;
		obj.determineCurFrame();
		if (rand & 511 == 0) {
			obj.state = STATE::DELAYEDSTART;
			obj.yPos = 0;
			obj.xPos = obj.xOrg;
			jjSamplePriority(SOUND::COMMON_BLUB1);
		} else {
			rand >>= 9;
			obj.xPos = obj.xPos + (rand & 15) - 7.5;
			rand >>= 4;
			if (rand & 1 == 1)
				obj.yPos = obj.yPos - 1;
		}
	}
}