Downloads containing DD-Rank.asc

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Diamondus UltimateFeatured Download DoubleGJ Tileset conversion 10 Download file
TSF with JJ2+ Only: Jungle UltimateFeatured Download DoubleGJ Tileset conversion 10 Download file

File preview

/* ===Demon Dash ranking script v0.6===
Written by Gnegon Galek, rank calculation adjustments by Seren, end transition by SpazElectro
This module also removes the extra lives system on easy mode.
Optionally, you can also set a time limit for the level depending on difficulty.
It works like in JJ1: if time runs out, you lose a life! Fortunately, you can continue from a checkpoint with the timer reset to full.
Hourglass now functions like in JJ1, ditching the pretty much useless player freezing effect.

---PUT IN LEVEL SCRIPT, MERGE WITH SAME FUNCTIONS IF NEEDED:---

const float maxScore = ?; // greatest REALISTICALLY OBTAINABLE score to obtain in the level (do not calculate, playtest)
const int bestTime = ?; // best REALISTICALLY OBTAINABLE level completion time in seconds (don't just blast thru the level with jjinv/jjgod)
const int easyTimer = 0; // leave as 0 for no time limit
const int normalTimer = 0;
const int hardTimer = 84000; // in ticks
const int turboTimer = 63000;

void onLevelLoad() {
	initiateRanking();
	initFade();
}

void onLevelBegin() {
	setTimer(); // can be left out if you don't want a time limit on any difficulty
}

void onLevelReload() {
	scoreSeconds = storeScoreSeconds; // maybe there's a way to have this work in cooperative, but I'm out of ideas
}

void onMain() {
	if (jjDifficulty == 0) {
		oneUpsIntoBlueGems();
	}
	if ((jjGameTicks % 70) == 0 && !levelOver) { // if used in tandem with DD-Order.asc, it's gonna need "&& CurrentPopup is null" too
		scoreSeconds++;
	}	
	rankingSetup();
	if (levelOver) updateFade();
}

bool onDrawScore(jjPLAYER@ player, jjCANVAS@ canvas) {
	if (levelOver) {
		drawFade(player, canvas);
		if (rankLine < 6) rankingDisplay(player, canvas);
	}
	if (rankLine >= 6) return true;
	else return false;
}

bool onDrawHealth(jjPLAYER@ player, jjCANVAS@ canvas) {
	if (rankLine >= 6) return true;
	else return false;
}

bool onDrawAmmo(jjPLAYER@ player, jjCANVAS@ canvas) {
	if (rankLine >= 6) return true;
	else return false;
}

bool onDrawLives(jjPLAYER@ player, jjCANVAS@ canvas) { 
	if (jjDifficulty == 0) { // "&& CurrentPopup is null" when used with DD-Order.asc
		infiniteLives(player, canvas); // can be left out if you don't want to display the life icon at all, like in online co-op
		return true;
	}
	if (rankLine >= 6) return true;
    else return false; // or return "CurrentPopup !is null && !CurrentPopup.DrawHUD();" when used with DD-Order.asc
}

bool onCheat(string &in cheat) {
	if (levelOver) return true; // avoid bugs caused by getting out of end screen
	else return false;
}

void onPlayer(jjPLAYER@ player) {
	rankingInterface(player);
}

void onPlayerDraw(jjPLAYERDRAW& draw) {
	jjPLAYER@ play = draw.player;
	if (isFading) {
		draw.sprite = false;
		return;
	}
}

---CALL THIS FUNCTION WHEN YOU WANT TO END THE LEVEL:---
endLevel(player, warp EOL = false); // must be tied to player that triggered level end!!
*/

#pragma require "children-yay.wav"
#pragma require "party-horn.wav"
#pragma require "wow.wav"
#pragma require "weak-fart.wav"
#pragma require "STVcartoonfade_fade.png"
#pragma offer "DD-Rank.s3m"

///@Event 90=Hourglass |-|Goodies |Hour|Glass

bool levelOver, fanfarePlayed, jinglePlayed, wowPlayed, endLivesGiven, smallScreen, stampPlayed, isFading, warpAnim, exitLeftAnim = false;
int scoreSeconds, storeScoreSeconds, timerMsgCD, rightSlide, topSlide, bottomSlide, gemTotal;

float timeDifference, DEFAULT_FADE_SCALE = 0.0;
float rankStamp = 10.0;
array<int> startingScore(5, 0);
array<int> timeBonus(5, 0);
array<int> hitPenalty(5, -1000); // take into account start of level blink
array<int> textSlide(6, -160);
array<array<int>> displayGems(4, array<int>(3));

const int gemsForExtraLife = 500; // editable in case testing proves that's too much
int rankLine, leftMargin, leftIndent, topMargin, bigLine, smallLine, rankLineCD, levelScore, totalScore, timeBonusDisplay, hitPenaltyDisplay, gemChannel, displayMinutes, displaySeconds, gtfoAnim = 0;
int waitJingle = 400;

const float FADE_SCALE_STEP = 0.05;
float fadeScale = DEFAULT_FADE_SCALE;

void initiateRanking() {
	jjSampleLoad(SOUND::INTRO_BLOW, "weak-fart.wav");
	jjSampleLoad(SOUND::INTRO_BOEM1, "children-yay.wav");
	jjSampleLoad(SOUND::INTRO_BOEM2, "party-horn.wav");
	jjSampleLoad(SOUND::INTRO_IFEEL, "wow.wav");
	jjObjectPresets[OBJECT::SAVEPOST].behavior = savePost;
	for (int i = 0; i < (jjLocalPlayerCount + 1); i++) {
		startingScore[i] = jjLocalPlayers[i].score;
	}
}

void rankingSetup() {
	smallScreen = jjSubscreenWidth <= 400;
	leftMargin = int(jjSubscreenWidth / 16);
	leftIndent = int(smallScreen ? jjSubscreenWidth / 10 : jjSubscreenWidth / 12);
	topMargin = int(jjSubscreenHeight / 12);
	bigLine = int(jjSubscreenHeight / 12);
	smallLine = int(jjSubscreenHeight / 18);
	if (!levelOver) {
		rightSlide = int(jjSubscreenWidth * 1.2);
		topSlide = int(jjSubscreenHeight / 16) * -1;
		bottomSlide = jjSubscreenHeight + int(jjSubscreenHeight / 16);
	}
	if (levelOver && rankLine > 5) fade();
	if (levelOver && rankLineCD > 0) rankLineCD--;
	if (jinglePlayed && waitJingle > 0) waitJingle--;
	if (waitJingle == 0 && !isFading) jjMusicLoad("DD-Rank.s3m");
	if (isFading) jjMusicStop();
}

void rankingDisplay(jjPLAYER@ player, jjCANVAS@ canvas) {
	if (topSlide < jjSubscreenHeight / 32) { topSlide += 2; } // we'll get the precise positioning later, mmkay?
	if (bottomSlide > jjSubscreenHeight - (jjSubscreenHeight / 32)) { bottomSlide -= 2; } // mirrors line above so don't forget to adjust as well
	canvas.drawString(int(ceil(jjSubscreenWidth / 3)), topSlide, "LEVEL COMPLETE", smallScreen ? STRING::SMALL : STRING::MEDIUM, STRING::BOUNCE, smallScreen? 8 : 16);
	canvas.drawString(int(ceil(jjSubscreenWidth / 6)), bottomSlide, "Press Fire or Select to continue", smallScreen ? STRING::SMALL : STRING::MEDIUM, STRING::BOUNCE, smallScreen? 8 : 16);
		
	if (rightSlide > jjSubscreenWidth * 0.65) { rightSlide -= 8; }
	if (displayGems[player.localPlayerID][0] < player.gems[GEM::RED] || displayGems[player.localPlayerID][1] < player.gems[GEM::GREEN] || displayGems[player.localPlayerID][2] < player.gems[GEM::BLUE]) {
		gemChannel = jjSampleLooped(jjLocalPlayers[0].xPos, jjLocalPlayers[0].yPos, SOUND::COMMON_PICKUP1, gemChannel);
	}

	if ((displayGems[player.localPlayerID][0] < player.gems[GEM::RED]) && jjGameTicks % 5 == 0) { displayGems[player.localPlayerID][0]++;	}
	if ((displayGems[player.localPlayerID][1] < player.gems[GEM::GREEN]) && jjGameTicks % 5 == 0) { displayGems[player.localPlayerID][1]++; }
	if ((displayGems[player.localPlayerID][2] < player.gems[GEM::BLUE]) && jjGameTicks % 5 == 0) { displayGems[player.localPlayerID][2]++; }

	canvas.drawString(rightSlide, topMargin + smallLine + bigLine, "x " + formatInt(displayGems[player.localPlayerID][0]), smallScreen ? STRING::SMALL : STRING::MEDIUM);
	canvas.drawSprite(rightSlide - 24, topMargin + smallLine + bigLine, ANIM::PICKUPS, 22, (jjGameTicks/10) % 7, 0, SPRITE::GEM, 0);
	canvas.drawString(rightSlide, topMargin + (smallLine * 2) + (bigLine * 2), "x " + formatInt(displayGems[player.localPlayerID][1]), smallScreen ? STRING::SMALL : STRING::MEDIUM);
	canvas.drawSprite(rightSlide - 24, topMargin + (smallLine * 2) + (bigLine * 2), ANIM::PICKUPS, 22, (jjGameTicks/10) % 7, 0, SPRITE::GEM, 1);
	canvas.drawString(rightSlide, topMargin + (smallLine * 3) + (bigLine * 3), "x " + formatInt(displayGems[player.localPlayerID][2]), smallScreen ? STRING::SMALL : STRING::MEDIUM);
	canvas.drawSprite(rightSlide - 24, topMargin + (smallLine * 3) + (bigLine * 3), ANIM::PICKUPS, 22, (jjGameTicks/10) % 7, 0, SPRITE::GEM, 2);
		
	canvas.drawString(rightSlide, topMargin + (smallLine * 4) + (bigLine * 4), "TOTAL GEMS", smallScreen ? STRING::SMALL : STRING::MEDIUM, STRING::BOUNCE, smallScreen? 8 : 16);
	canvas.drawString(rightSlide, topMargin + (smallLine * 5) + (bigLine * 4), formatInt(displayGems[player.localPlayerID][0] + (displayGems[player.localPlayerID][1] * 5) + (displayGems[player.localPlayerID][2] * 10)), smallScreen ? STRING::SMALL : STRING::MEDIUM);
		
	if (textSlide[0] < leftMargin) textSlide[0] += 8;
	else if (textSlide[0] > leftMargin) textSlide[0] = leftMargin; // fix overshooting
	canvas.drawString(textSlide[0], topMargin, "Level Score", smallScreen ? STRING::SMALL : STRING::MEDIUM, STRING::BOUNCE, smallScreen? 8 : 16);
	if (levelScore < player.score - startingScore[player.localPlayerID]) levelScore += 100;
	else if (levelScore > player.score - startingScore[player.localPlayerID]) levelScore = player.score - startingScore[player.localPlayerID]; // also fix overshooting...
	else if (rankLine == 0 && rankLineCD == 0) {
		rankLineCD = 60;
		rankLine++;	
	}
	canvas.drawString(textSlide[0], topMargin + smallLine, formatInt(levelScore), smallScreen ? STRING::SMALL : STRING::MEDIUM);
		
	if (rankLine >= 1) {
		if (textSlide[1] < leftMargin) textSlide[1] += 8;
		else if (textSlide[1] > leftMargin) textSlide[1] = int(leftMargin);
		levelScore = player.score - startingScore[player.localPlayerID];
		canvas.drawString(textSlide[1], topMargin + smallLine + bigLine, "Completion Time", smallScreen ? STRING::SMALL : STRING::MEDIUM, STRING::BOUNCE, smallScreen? 8 : 16);
		canvas.drawString(textSlide[1], topMargin + (smallLine * 2) + bigLine, formatInt(displayMinutes) + ":" + formatInt(displaySeconds, '0', 2), smallScreen ? STRING::SMALL : STRING::MEDIUM);
		if (displayMinutes < int(scoreSeconds / 60)) {
			displaySeconds++;
			if (displaySeconds == 60) {
				displayMinutes++;
				displaySeconds = 0;
			}
		}
		else if (displaySeconds < scoreSeconds % 60) displaySeconds++;
		else if (rankLine == 1 && rankLineCD == 0) {
			rankLineCD = 60;
			rankLine++;	
		}
	}
		
	if (rankLine >= 2) {
		if (textSlide[2] < leftMargin) textSlide[2] += 8;
		else if (textSlide[2] > leftMargin) textSlide[2] = leftMargin;
		displayMinutes = int(scoreSeconds / 60);
		displaySeconds = scoreSeconds % 60;
		canvas.drawString(textSlide[2], topMargin + (smallLine * 2) + (bigLine * 2), "Time Bonus", smallScreen ? STRING::SMALL : STRING::MEDIUM, STRING::BOUNCE, smallScreen? 8 : 16);
		if (timeBonusDisplay < timeBonus[player.localPlayerID]) timeBonusDisplay += 100;
		else if (timeBonusDisplay > timeBonus[player.localPlayerID]) timeBonusDisplay = timeBonus[player.localPlayerID];
		else if (rankLine == 2 && rankLineCD == 0) {
			rankLineCD = 60;
			rankLine++;	
		}
		canvas.drawString(textSlide[2], topMargin + (smallLine * 3) + (bigLine * 2), formatInt(timeBonusDisplay), smallScreen ? STRING::SMALL : STRING::MEDIUM);
	}
		
	if (rankLine >= 3) {
		if (textSlide[3] < leftMargin) textSlide[3] = textSlide[3] + 8;
		else if (textSlide[3] > leftMargin) textSlide[3] = leftMargin;
		timeBonusDisplay = timeBonus[player.localPlayerID];
		canvas.drawString(textSlide[3], topMargin + (smallLine * 3) + (bigLine * 3), "Hit Penalty", smallScreen ? STRING::SMALL : STRING::MEDIUM, STRING::BOUNCE, smallScreen? 8 : 16);
		if (hitPenaltyDisplay > hitPenalty[player.localPlayerID] * -1) hitPenaltyDisplay -= 100; // this is always a multiple of 1000 so not doing overshoot check
		if (hitPenaltyDisplay == 0 && textSlide[3] == leftMargin && !wowPlayed) {
			jjSamplePriority(SOUND::INTRO_IFEEL);
			int yoinky = int(jjRandom() % 8 + 8);
			for (int i = 0; i < yoinky; i++) {
				jjPARTICLE@ particle = jjAddParticle(PARTICLE::STAR);
				particle.xPos = player.cameraX + leftMargin + (smallScreen ? jjAnimFrames[jjAnimations[jjAnimSets[ANIM::FONT].firstAnim + 1].firstFrame + 16].width / 2 : jjAnimFrames[jjAnimations[jjAnimSets[ANIM::FONT].firstAnim].firstFrame + 16].width / 2);
				particle.yPos = player.cameraY + topMargin + (smallLine * 4) + (bigLine * 3) + (smallScreen ? 8 : 4);
				particle.xSpeed = int(jjRandom() % 9 - 4); // don't ask me why it needs to be converted to int. it just does
				particle.ySpeed = int(jjRandom() % 9 - 4);
				particle.star.angularSpeed = int(jjRandom() % 4 + 1);
				particle.star.color = int(jjRandom() % 3 + 64);
				particle.star.size = int(jjRandom() % 16 + 24);
			}
			wowPlayed = true;
		}
		if (rankLine == 3 && rankLineCD == 0 && hitPenaltyDisplay == hitPenalty[player.localPlayerID] * -1) {
			rankLineCD = 60;
			rankLine++;	
		}
		canvas.drawString(textSlide[3], topMargin + (smallLine * 4) + (bigLine * 3), formatInt(hitPenaltyDisplay), smallScreen ? STRING::SMALL : STRING::MEDIUM);
	}
		
	if (wowPlayed) {
		canvas.drawString(leftMargin + leftIndent, topMargin + (smallLine * 4) + (bigLine * 3), "FLAWLESS!!", smallScreen ? STRING::SMALL : STRING::MEDIUM, STRING::BOUNCE, smallScreen? 8 : 16);
	}
		
	if (rankLine >= 4) {
		if (textSlide[4] < leftMargin) textSlide[4] += 8;
		else if (textSlide[4] > leftMargin) textSlide[4] = leftMargin;
		hitPenaltyDisplay = hitPenalty[player.localPlayerID] * -1;
		canvas.drawString(textSlide[4], topMargin + (smallLine * 4) + (bigLine * 4), "Total", smallScreen ? STRING::SMALL : STRING::MEDIUM, STRING::BOUNCE, smallScreen? 8 : 16);
		if (totalScore < player.score - startingScore[player.localPlayerID] + timeBonus[player.localPlayerID] - hitPenalty[player.localPlayerID]) totalScore = totalScore + 100;
		else if (totalScore > player.score - startingScore[player.localPlayerID] + timeBonus[player.localPlayerID] - hitPenalty[player.localPlayerID]) { totalScore = player.score - startingScore[player.localPlayerID] + timeBonus[player.localPlayerID] - hitPenalty[player.localPlayerID]; }
		else if (rankLine == 4 && rankLineCD == 0) {
			rankLineCD = 60;
			rankLine++;
		}
		canvas.drawString(textSlide[4], topMargin + (smallLine * 5) + (bigLine * 4), formatInt(totalScore), smallScreen ? STRING::SMALL : STRING::MEDIUM);
			
		displayGems[player.localPlayerID][0] = player.gems[GEM::RED];
		displayGems[player.localPlayerID][1] = player.gems[GEM::GREEN];
		displayGems[player.localPlayerID][2] = player.gems[GEM::BLUE];
		gemTotal = displayGems[player.localPlayerID][0] + (displayGems[player.localPlayerID][1] * 5) + (displayGems[player.localPlayerID][2] * 10);
		if (jjDifficulty > 0) {
			canvas.drawSprite(int(jjSubscreenWidth * 0.65) + 10, topMargin + (smallLine * 5) + (bigLine * 5), ANIM::PICKUPS, 0, (jjGameTicks/7) % 9);
			canvas.drawString(int(jjSubscreenWidth * 0.65) + 32, topMargin + (smallLine * 5) + (bigLine * 5), "x " + formatInt(gemTotal / gemsForExtraLife), smallScreen ? STRING::SMALL : STRING::MEDIUM);
			if ((int(gemTotal / gemsForExtraLife) > 0) && !endLivesGiven) {
				player.lives = player.lives + int(gemTotal / gemsForExtraLife);
				jjPARTICLE@ particle = jjAddParticle(PARTICLE::STRING);
				if (particle !is null) {
					particle.xPos = player.cameraX + int(jjSubscreenWidth * 0.65) + 10;
					particle.yPos = player.cameraY + topMargin + (smallLine * 5) + (bigLine * 5);
					particle.xSpeed = (-65536 - int(jjRandom() & 0x3FFF)) / 65536.f; // negative
					particle.ySpeed = (-7168 - int(jjRandom() & 0x7FFF)) / -65536.f; // positive
					particle.string.text = formatInt(gemTotal / gemsForExtraLife) + "UP!";
				}
				jjSamplePriority(SOUND::COMMON_HARP1);
				endLivesGiven = true;
			}
		}
	}
		
	if (rankLine >= 5) {
		if (textSlide[5] < leftMargin) textSlide[5] += 8;
		else if (textSlide[5] > leftMargin) textSlide[5] = leftMargin;
		totalScore = player.score - startingScore[player.localPlayerID] + timeBonus[player.localPlayerID] - hitPenalty[player.localPlayerID];
		canvas.drawString(textSlide[5], topMargin + (smallLine * 5) + (bigLine * 5), "RANK", smallScreen ? STRING::SMALL : STRING::MEDIUM, STRING::BOUNCE, smallScreen? 8 : 16);
		if (rankStamp > 1) rankStamp -= 0.4;
		else if (rankStamp < 1) rankStamp = 1; // just in case??
		else if (!stampPlayed) {
			jjSamplePriority(SOUND::COMMON_LAND1);
			stampPlayed = true;
		}
		if ((player.score - startingScore[player.localPlayerID] + timeBonus[player.localPlayerID] - hitPenalty[player.localPlayerID]) * jjLocalPlayerCount >= maxScore * 1.5) {
			canvas.drawResizedSprite(leftIndent, topMargin + (smallLine * 6) + (bigLine * 5), ANIM::FONT, smallScreen ? 0 : 2, 51, rankStamp, rankStamp, SPRITE::PALSHIFT, 232);
			if (!fanfarePlayed && rankStamp == 1) {
				jjSamplePriority(SOUND::INTRO_BOEM1);
				int sploinky = int(jjRandom() % 24 + 24);
				for (int i = 0; i < sploinky; i++) {
					jjPARTICLE@ particle = jjAddParticle(PARTICLE::STAR);
					particle.xPos = player.cameraX + leftIndent + (smallScreen ? jjAnimFrames[jjAnimations[jjAnimSets[ANIM::FONT].firstAnim].firstFrame + 51].width / 2 : jjAnimFrames[jjAnimations[jjAnimSets[ANIM::FONT].firstAnim + 2].firstFrame + 16].width / 2);
					particle.yPos = player.cameraY + topMargin + (smallLine * 6) + (bigLine * 5) + (smallScreen ? jjAnimFrames[jjAnimations[jjAnimSets[ANIM::FONT].firstAnim].firstFrame + 51].height / 2 : jjAnimFrames[jjAnimations[jjAnimSets[ANIM::FONT].firstAnim + 2].firstFrame + 16].height / 2);
					particle.xSpeed = int(jjRandom() % 9 - 4);
					particle.ySpeed = int(jjRandom() % 9 - 4);
					particle.star.angularSpeed = int(jjRandom() % 4 + 1);
					particle.star.color = int(jjRandom() % 3 + 40);
					particle.star.size = int(jjRandom() % 24 + 24);
				}
				fanfarePlayed = true;
			}
		} else if ((player.score - startingScore[player.localPlayerID] + timeBonus[player.localPlayerID] - hitPenalty[player.localPlayerID]) * jjLocalPlayerCount > maxScore * 1.3) {
			canvas.drawResizedSprite(leftIndent, topMargin + (smallLine * 6) + (bigLine * 5), ANIM::FONT, smallScreen ? 0 : 2, 33, rankStamp, rankStamp, SPRITE::PALSHIFT, 208);
			if (!fanfarePlayed && rankStamp == 1) {
				jjSamplePriority(SOUND::INTRO_BOEM2);
				int doinky = int(jjRandom() % 12 + 12);
					for (int i = 0; i < doinky; i++) {
					jjPARTICLE@ particle = jjAddParticle(PARTICLE::STAR);
					particle.xPos = player.cameraX + leftIndent + (smallScreen ? jjAnimFrames[jjAnimations[jjAnimSets[ANIM::FONT].firstAnim].firstFrame + 33].width / 2 : jjAnimFrames[jjAnimations[jjAnimSets[ANIM::FONT].firstAnim + 2].firstFrame + 33].width / 2);
					particle.yPos = player.cameraY + topMargin + (smallLine * 6) + (bigLine * 5) + (smallScreen ? jjAnimFrames[jjAnimations[jjAnimSets[ANIM::FONT].firstAnim].firstFrame + 33].height / 2 : jjAnimFrames[jjAnimations[jjAnimSets[ANIM::FONT].firstAnim + 2].firstFrame + 33].height / 2);
					particle.xSpeed = int(jjRandom() % 9 - 4);
					particle.ySpeed = int(jjRandom() % 9 - 4);
					particle.star.angularSpeed = int(jjRandom() % 4 + 1);
					particle.star.color = int(jjRandom() % 3 + 16);
					particle.star.size = int(jjRandom() % 16 + 24);
					}
					fanfarePlayed = true;
			}
		} else if ((player.score - startingScore[player.localPlayerID] + timeBonus[player.localPlayerID] - hitPenalty[player.localPlayerID]) * jjLocalPlayerCount > maxScore * 1.1) {
			canvas.drawResizedSprite(leftIndent, topMargin + (smallLine * 6) + (bigLine * 5), ANIM::FONT, smallScreen ? 0 : 2, 34, rankStamp, rankStamp, SPRITE::PALSHIFT, 216);
		} else if ((player.score - startingScore[player.localPlayerID] + timeBonus[player.localPlayerID] - hitPenalty[player.localPlayerID]) * jjLocalPlayerCount > maxScore * 0.9) {
			canvas.drawResizedSprite(leftIndent, topMargin + (smallLine * 6) + (bigLine * 5), ANIM::FONT, smallScreen ? 0 : 2, 35, rankStamp, rankStamp, SPRITE::PALSHIFT, 224);
		} else if ((player.score - startingScore[player.localPlayerID] + timeBonus[player.localPlayerID] - hitPenalty[player.localPlayerID]) * jjLocalPlayerCount > maxScore * 0.7) {
			canvas.drawResizedSprite(leftIndent, topMargin + (smallLine * 6) + (bigLine * 5), ANIM::FONT, smallScreen ? 0 : 2, 36, rankStamp, rankStamp, SPRITE::PALSHIFT, 16);
		} else {
			canvas.drawResizedSprite(leftIndent, topMargin + (smallLine * 6) + (bigLine * 5), ANIM::FONT, smallScreen ? 0 : 2, 37, rankStamp, rankStamp, SPRITE::PALSHIFT, 8);
			if (!fanfarePlayed && rankStamp == 1) {
				jjSamplePriority(SOUND::INTRO_BLOW);
				fanfarePlayed = true; // well you could say that
			}
		}
	}
}

void rankingInterface(jjPLAYER@ player) {
	if (jjDifficulty == 0 && player.lives < 3) player.lives = 3;
	if (player.blink == -207) hitPenalty[player.localPlayerID] += 1000;
	if (levelOver) {
		player.timerStop();
		player.invincibility = -15;
		player.idle = 0;
		player.noFire = true;
		jjCharacters[player.charCurr].canRun = false; // block revving
		player.keyLeft = player.keyRight = player.keyDown = player.keyUp = player.keyRun = player.keyJump = false;
		if ((player.keyFire || player.keySelect || jjKey[1]) && rankLineCD == 0) {
			rankLineCD = 60;
			rankLine++;
			player.keySelect = player.keyFire = false;
		}
	}
}

void savePost(jjOBJ@ save) {
	switch(save.state) {
	case STATE::START:
		timerMsgCD = 0;
		break;
	case STATE::ACTION:
		if (timerMsgCD == 0) { 
			if (jjLocalPlayerCount == 1) { // not abusable in coop this way until onLevelReload is improved 
			storeScoreSeconds = scoreSeconds;
			}
			jjAlert(formatInt(scoreSeconds / 60) + ":" + formatInt(scoreSeconds % 60, '0', 2));
			timerMsgCD = 100;
		}
		break;
	case STATE::DONE:
		if (timerMsgCD > 0) { timerMsgCD--; }
		break;
	}	
	save.behave(BEHAVIOR::CHECKPOINT);
}

void endLevel(jjPLAYER@ player, bool warpEnd = false, bool exitLeft = false) {	
	if (player.charCurr != player.charOrig) player.revertMorph();
	player.fly = FLIGHT::NONE;
	levelOver = true;
	exitLeftAnim = exitLeft;
	if (scoreSeconds > (bestTime * 3)) timeBonus[player.localPlayerID] = 0;
	else if (scoreSeconds > bestTime) {
		timeDifference = bestTime * 3 - scoreSeconds; // level is over once any player finishes, so don't need scoreSeconds to be an array
		timeBonus[player.localPlayerID] = int(ceil(maxScore * (timeDifference / (bestTime * 2)) / 100) * 100);
	} else timeBonus[player.localPlayerID] = maxScore;
	if (!jinglePlayed) {
		jjMusicStop();
		switch(player.charCurr) {
			case CHAR::JAZZ:
				jjSamplePriority(SOUND::ENDTUNEJAZZ_TUNE);
				break;
			case CHAR::SPAZ:
				jjSamplePriority(SOUND::ENDTUNESPAZ_TUNE);
				break;
			case CHAR::LORI:
				jjSamplePriority(SOUND::ENDTUNELORI_CAKE);
				break;
		}
		jinglePlayed = true;
	}
	warpAnim = warpEnd;
}

void initFade() {
	jjANIMSET@ fadeAnim = jjAnimSets[ANIM::CUSTOM[255]];
	fadeAnim.allocate(array<uint>={1});
	fadeAnim.load(
		jjPIXELMAP("STVcartoonfade_fade.png"),
		frameWidth: 600,
		frameHeight: 600,
		frameSpacingX: 0,
		frameSpacingY: 0,
		startY: 0,
		firstAnimToOverwrite: jjAnimSets[ANIM::CUSTOM[255]]
	);
}

void updateFade() {
	if (isFading) {
		fadeScale += FADE_SCALE_STEP;

		if (fadeScale >= 4) {
			isFading = false;
		}
	}
	if (!isFading && fadeScale >= 4 && gtfoAnim < 154) {
		if (gtfoAnim == 1) {
			if (warpAnim) jjSamplePriority(SOUND::COMMON_TELPORT1);
			else jjSamplePriority(SOUND::COMMON_REVUP);
		}
		if (gtfoAnim == 69) jjSamplePriority(SOUND::AMMO_BULFL3); // nice
		gtfoAnim++;
	}
	if (gtfoAnim == 49 && warpAnim) jjNxt(false, true); // you'll need to manually make the player warp in at the start of next level but this is more reliable than the original effect
	if (gtfoAnim == 154 && !warpAnim) jjNxt(false, true);
}

void drawFade(jjPLAYER@ player, jjCANVAS@ canvas) {
	int adjustedXPos = int(player.xPos) - int(player.cameraX);
	int adjustedYPos = int(player.yPos) - int(player.cameraY);

	canvas.drawResizedSprite(
		adjustedXPos, adjustedYPos,
		ANIM::CUSTOM[255], 0, 0,
		fadeScale, fadeScale,
		SPRITE::ALPHAMAP, 1
	);
	if (isFading) {
		switch(player.charOrig) {
		case CHAR::JAZZ:
			canvas.drawSprite(adjustedXPos, adjustedYPos, ANIM::JAZZ, RABBIT::STAND, player.curFrame, exitLeftAnim ? -1 : 1);
			break;
		case CHAR::SPAZ:
			canvas.drawSprite(adjustedXPos, adjustedYPos, ANIM::SPAZ, RABBIT::STAND, player.curFrame, exitLeftAnim ? -1 : 1);
			break;
		case CHAR::LORI:
			canvas.drawSprite(adjustedXPos, adjustedYPos, ANIM::LORI, RABBIT::STAND, player.curFrame, exitLeftAnim ? -1 : 1);
			break;
		}
	}
	if (!isFading && fadeScale >= 4 && gtfoAnim < 154 && !warpAnim) {
		switch(player.charOrig) {
		case CHAR::JAZZ:
			canvas.drawSprite(adjustedXPos, adjustedYPos, ANIM::JAZZ, RABBIT::ENDOFLEVEL, uint8(gtfoAnim / 7), exitLeftAnim ? -1 : 1);
			break;
		case CHAR::SPAZ:
			canvas.drawSprite(adjustedXPos, adjustedYPos, ANIM::SPAZ, RABBIT::ENDOFLEVEL, uint8(gtfoAnim / 7), exitLeftAnim ? -1 : 1);
			break;
		case CHAR::LORI:
			canvas.drawSprite(adjustedXPos, adjustedYPos, ANIM::LORI, RABBIT::ENDOFLEVEL, uint8(gtfoAnim / 7), exitLeftAnim ? -1 : 1);
			break;
		}
	}
	if (!isFading && fadeScale >= 4 && gtfoAnim < 49 && warpAnim) {
		switch(player.charOrig) {
		case CHAR::JAZZ:
			canvas.drawSprite(adjustedXPos, adjustedYPos, ANIM::JAZZ, RABBIT::TELEPORT, uint8(gtfoAnim / 7), exitLeftAnim ? -1 : 1);
			break;
		case CHAR::SPAZ:
			canvas.drawSprite(adjustedXPos, adjustedYPos, ANIM::SPAZ, RABBIT::TELEPORT, uint8(gtfoAnim / 7), exitLeftAnim ? -1 : 1);
			break;
		case CHAR::LORI:
			canvas.drawSprite(adjustedXPos, adjustedYPos, ANIM::LORI, RABBIT::TELEPORT, uint8(gtfoAnim / 7), exitLeftAnim ? -1 : 1);
			break;
		}
	}
}

void fade() {
	isFading = true;
}

void infiniteLives(jjPLAYER@ player, jjCANVAS@ canvas) {
	int charHead;
	switch(player.charCurr) {
		case CHAR::BIRD2: charHead = 0; break;
		case CHAR::BIRD: charHead = 1; break;
		case CHAR::FROG: charHead = 2; break;
		case CHAR::JAZZ: charHead = 3; break;
		case CHAR::LORI: charHead = 4; break;
		case CHAR::SPAZ: charHead = 5; break;
	}
	canvas.drawSprite(0, int(jjSubscreenHeight), ANIM::FACES, charHead, (jjGameTicks/7) % 36);
	if (smallScreen) canvas.drawString(32, int(jjSubscreenHeight - 9), "x^");
	else canvas.drawString(32, int(jjSubscreenHeight - 14), "x^", STRING::MEDIUM);
}

void oneUpsIntoBlueGems() {
	for (int i = jjObjectCount; --i > 0;) {
		jjOBJ@ obj = jjObjects[i];
		if (obj.eventID == 80 && obj.isActive) {
			jjOBJ@ lifegem = jjObjects[jjAddObject(OBJECT::BLUEGEM, obj.xPos, obj.yPos)];
			lifegem.deactivates = false;
			obj.delete();
		}
	}
}

void setTimer() {
	for (int i = 0; i < (jjLocalPlayerCount + 1); i++) {
		if (jjDifficulty == 0 && easyTimer > 0) jjLocalPlayers[i].timerStart(easyTimer, false);
		if (jjDifficulty == 1 && normalTimer > 0) jjLocalPlayers[i].timerStart(normalTimer, false);
		if (jjDifficulty == 2 && hardTimer > 0) jjLocalPlayers[i].timerStart(hardTimer, false);
		if (jjDifficulty == 3 && turboTimer > 0) jjLocalPlayers[i].timerStart(turboTimer, false);
		jjLocalPlayers[i].timerPersists = true;
	}
	jjObjectPresets[OBJECT::FREEZER].points = 500;
	jjObjectPresets[OBJECT::FREEZER].scriptedCollisions = true;
	jjObjectPresets[OBJECT::FREEZER].behavior = timerExtend();
}

void onPlayerTimerEnd(jjPLAYER@ player) {
	player.kill();
	switch (jjDifficulty) {
		case 0: player.timerStart(easyTimer + 140, false); break;
		case 1: player.timerStart(normalTimer + 140, false); break;
		case 2: player.timerStart(hardTimer + 140, false); break;
		case 3: player.timerStart(turboTimer + 140, false); break;
	}
}

class timerExtend : jjBEHAVIORINTERFACE {
int addedTime;
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::PICKUP);
		switch (jjDifficulty) {
		case 0: addedTime = 8; break;
		case 1: addedTime = 4; break;
		case 2: addedTime = 2; break;
		case 3: addedTime = 1; break;
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@, jjPLAYER@ player, int) {
		player.frozen = 0;
		player.timerTime += 4200 * addedTime; 
		jjPARTICLE@ particle = jjAddParticle(PARTICLE::STRING);
		if (particle !is null) {
			particle.xPos = obj.xPos;
			particle.yPos = obj.yPos;
			particle.xSpeed = (-32768 - int(jjRandom() & 0x3FFF)) / -65536.f; // positive
			particle.ySpeed = (-65536 - int(jjRandom() & 0x7FFF)) / 65536.f; // negative
			particle.string.text = "+" + formatInt(addedTime) + "min!";
		}
		obj.behavior = BEHAVIOR::EXPLOSION2;
		obj.scriptedCollisions = false;
		obj.frameID = 0;
		jjSample(obj.xPos, obj.yPos, SOUND::COMMON_PICKUP1);
		return true;
	}
}