Downloads containing Fio4_b.j2as

Downloads
Name Author Game Mode Rating
JJ2+ Only: Find It Out (Single Player)Featured Download Superjazz Single player 8.7 Download file

File preview

const bool MLLESetupSuccessful = MLLE::Setup(); ///@MLLE-Generated
#include "MLLE-Include-1.6.asc" ///@MLLE-Generated
#pragma require "Fio4_b-MLLE-Data-1.j2l" ///@MLLE-Generated
#pragma require "Inferno1.j2t" ///@MLLE-Generated
#pragma require "wbrg.j2t" ///@MLLE-Generated
#pragma require "Fio4_b.j2l" ///@MLLE-Generated
#pragma require "Boss2.j2b"
#include "Fio_common.asc"
#include "Fio_cutscene.asc"
#include "Fio_drawing.asc"
#include "Fio_entities.asc"
#include "Fio_globals.asc"
#include "Fio_utils.asc"

enum Cutscene { CUTSCENE_NONE, CUTSCENE_INTRO, CUTSCENE_BOSS, CUTSCENE_OUTRO };

class CoolingIce : jjBEHAVIORINTERFACE {
	CoolingIce(jjOBJ@ obj) {
		obj.behavior = this;
	}
	void onBehave(jjOBJ@ obj) override {
		switch(obj.state) {
			case STATE::START:
				if (obj.ySpeed < 0) { // Upwards
					if (!hasPlayerShotIceAmmoUpwardsInBossBattle) {
						hasPlayerShotIceAmmoUpwardsInBossBattle = true;
						fioDraw::doShowText(5);
					}
					obj.ySpeed = 1.5;
					
					if (obj.eventID == OBJECT::ICEBULLETPU) {
						if (playerHeatLevel > 1) {
							playerHeatLevel -= 2;
						} else {
							playerHeatLevel = 0;
						}
					} else {
						if (playerHeatLevel > 0) {
							playerHeatLevel--;
						} else {
							playerHeatLevel = 0;
						}
					}
					
					playerHeatLevelElapsed = 0;
				}
				break;
		}
		obj.behave(BEHAVIOR::BULLET);
	}
}

const float BOSS_X = TILE * 45.5;
const float BOSS_Y = TILE * 61.25;
const float CUTSCENE_BOSS_X = TILE * 32;
const float CUTSCENE_BOSS_Y = TILE * 61;
const float DURATION_FADE_TOTAL = CUTSCENE_SECOND * 4;
const float DURATION_FADE_BLACKOUT = CUTSCENE_SECOND * 2;
const float INTRO_ROLL_X = TILE * 221;
const float INTRO_ROLL_Y = TILE * 40;
const float INTRO_X = TILE * 185;
const float INTRO_Y = TILE * 62;
const float PLATFORM_OFFSET = 26;
const float RABBIT_MIND_STONE_DURATION = CUTSCENE_SECOND * 8 - 3;
const float RABBIT_ROLL_DURATION = CUTSCENE_SECOND * 16;
const float RABBIT_STILL_DURATION_INTRO = CUTSCENE_SECOND * 10 - 22;

const uint MAX_HEAT_LEVEL = 5;

const uint8 HEAT_WARNING_MAX_INTENSITY_LEVEL_3 = 64;
const uint8 HEAT_WARNING_MAX_INTENSITY_LEVEL_4 = 96;
const uint8 HEAT_WARNING_MAX_INTENSITY_LEVEL_5 = 128;

const string NEXT_LEVEL_FILENAME = "Fio4_y.j2l";

uint playerHeatLevel = 1;
uint playerHeatLevelElapsed = 0;
uint playerHeatLevelIncreaseInterval = 350;

uint8 activeCutscene = uint8(CUTSCENE_NONE);
uint8 playerHeatLevelFade = 0;

bool hasBossFreezeTextBeenDisplayed = false;
bool hasHeatTextBeenDisplayed = false;
bool hasHeatTextBeenDisplayedNoIce = false;
bool hasIceSourceTextBeenShown = false;
bool hasPlayerShotIceAmmoUpwardsInBossBattle = false;
bool hasPlayerSteppedOnFirstPlatform = false;
bool isMindStoneVisible = false;
bool isPlayerHeatLevelFadingIn = false;
bool isPlayerRenderedInAir = false;
bool wasCutsceneWatchedTillTheEndBoss = false;

ANIM::Set playerAnimSet;

BossFromHell@ bubba;
CharacterWithMindStone@ character;

array<string> cutsceneTextsBoss = {
	"||What? Is that really you again? You should have been dead long ago!",
	"|Look! I have no intention to fight you again. I am just searching for a way to find someone from the underworld.",
	"||Oh, then in any case this will be really simple. If you defeat me again, you'll be drawn deeper into the underworld, straight into a prison cell!",
	"||Perhaps you'll find your miserable friend from the cell next to you! Hah hah!",
	"||However, you don't even have to worry about that, because I will scorch your soul right here in my lair! No living soul can survive the heat for long!",
	"|I guess you are right...It is getting quite hot here. Then this battle shall be quick!",
	"||PREPARE TO DIE!"
};

array<string> cutsceneTextsIntro = {
	"||||After getting in contact with the mystic energies in the darkness, you've once again entered a journey through space, wondering where you'll land this time.",
	"||||During your travel, you notice that for a change, these energies do not hurt you. After everything you've gone through, you feel safe and sound for the first time in a long while.",
	"||||Once the energies finally calm down, you notice a change in the surrounding temperature.",
	"", // Empty for visuality
	"|Woah! This place is the total opposite from the previous one, yet it's still quite dark! I wonder if I'm anywhere closer to Nicholas yet.",
	"|Looks like I've lost contact with Nicholas again. Hopefully he is alright. If something bad has happened to him, I might never see the light of day again...",
	"|I must push forward. My time to act may be limited."
};

array<string> cutsceneTextsOutro = {
	"||YAAAAAARGGGGHHHHH!"
};

array<string> texts = {
	"|Ugh...more dancing with death. If I ever survive, I'll have a good tale to tell.",
	"|I wonder where this portal will take me to...",
	"|Another source of ice in such a warm place! I wonder what can I use it this time for...",
	"|Damn! I got nothing to cool off myself! This heat is killing me!",
	"|It's getting too hot in here. Perhaps I could shoot ice upwards over myself to cool off? Else this heat kills me!", // 4th
	"|Phew...that's better! I need to keep on cooling myself constantly!",
	"|It is too hot here to actually freeze anything. I should just shoot it upwards to cool myself."
};

array<Checkpoint@> fio4bCheckpoints = {
	Checkpoint(0, INTRO_X, INTRO_Y),
	Checkpoint(1, TILE * 154, TILE * 22),
	Checkpoint(2, TILE * 68, TILE * 14)
};

array<Platform@> bossPlatforms;
array<Platform@> platforms;

void controlBubba() {
	if (bubba.obj.freeze > 1) {
		bubba.obj.freeze = 1;
		
		if (!hasBossFreezeTextBeenDisplayed) {
			fioDraw::doShowText(6);
			hasBossFreezeTextBeenDisplayed = true;
		}
	}
}

void controlPlayerHeatLevel(jjPLAYER@ play) {
	if (playerHeatLevel >= MAX_HEAT_LEVEL) {
		playerHeatLevelElapsed = 0;
	} else if (playerHeatLevelElapsed < playerHeatLevelIncreaseInterval) {
		playerHeatLevelElapsed++;
	} else {
		playerHeatLevel++;
		playerHeatLevelElapsed = 0;
	}
	if (playerHeatLevel <= 3) {
		play.jumpStrength = -10;
	}
	if (playerHeatLevel >= 3) {
		if (play.xSpeed > 3) {
			play.xSpeed = 3;
		} else if (play.xSpeed < -3) {
			play.xSpeed = -3;
		}
		if (!hasHeatTextBeenDisplayed && !hasPlayerShotIceAmmoUpwardsInBossBattle && play.ammo[3] > 0) {
			fioDraw::doShowText(4);
			hasHeatTextBeenDisplayed = true;
		} else if (!hasHeatTextBeenDisplayedNoIce && play.ammo[3] == 0) {
			fioDraw::doShowText(3);
			hasHeatTextBeenDisplayedNoIce = true;
		}
	}
	if (playerHeatLevel >= 4) {
		play.jumpStrength = -6;
	}
	if (playerHeatLevel >= 5) {
		play.hurt(1);
	}
}

void drawIceSource(jjCANVAS@ canvas) {
	canvas.drawRotatedSprite(TILE * 72.5, TILE * 14, ANIM::AMMO, 82, 8, jjGameTicks % 1024, 2, 2, SPRITE::BLEND_NORMAL, 192);
}

void drawMindStoneAtIntro(jjCANVAS@ canvas) {
	canvas.drawTile(int(play.xPos) + character.mindStoneX, int(play.yPos) + character.mindStoneY, 965);
	canvas.drawTile(int(play.xPos) + character.mindStoneX, int(play.yPos) + character.mindStoneY + TILE, 975);
}

void endCutsceneIntro() {
	fioCut::endCutscene(INTRO_X, INTRO_Y);
	isMindStoneVisible = false;
	play.lighting = LIGHTING_TWILIGHT;
	activeCutscene = CUTSCENE_NONE;
	checkpoints[0].setReached();
	fioUtils::releasePlayer();
}

void endCutsceneBoss() {
	fioCut::endCutscene(CUTSCENE_BOSS_X, CUTSCENE_BOSS_Y);
	activeCutscene = CUTSCENE_NONE;
	fioCut::clearAnimationChains();
	staticPlayerRenderState = STATE_STATIC_PLAYER_RENDER_OFF;
	isPlayerHeatLevelFadingIn = false;
	playerHeatLevelFade = 0;
	fioUtils::releasePlayer();
	play.cameraUnfreeze(false);
	jjMusicLoad(BOSS_THEME_FILENAME, false, true);
	fioDraw::textIndex = -1;
	@bubba = BossFromHell(fio::getActiveObjectFromLevel(OBJECT::BUBBA), getBossHealthByDifficulty());
	bubba.obj.state = STATE::DELAYEDSTART;
	isCustomBossActivated = true;
	CoolingIce(jjObjectPresets[OBJECT::ICEBULLET]);
	CoolingIce(jjObjectPresets[OBJECT::ICEBULLETPU]);
	initiatePlatformMovementBoss();
	fio::preserveAmmoForBossBattle();
}

int getBossHealthByDifficulty() {
	if (jjDifficulty >= 3) return 600;
	if (jjDifficulty == 2) return 400;
	if (jjDifficulty == 1) return 300;
	return 200;
}

int getPointRewardByDifficulty() {
	if (jjDifficulty >= 3) return 60000;
	if (jjDifficulty == 2) return 10000;
	if (jjDifficulty == 1) return 5000;
	return 1000;
}

uint8 getHeatWarningIntensityLevel() {
	if (playerHeatLevel == 3) {
		return HEAT_WARNING_MAX_INTENSITY_LEVEL_3;
	}
	if (playerHeatLevel == 4) {
		return HEAT_WARNING_MAX_INTENSITY_LEVEL_4;
	}
	return HEAT_WARNING_MAX_INTENSITY_LEVEL_5;
}

void initiatePlatformMovement() {
	for (uint i = 0; i < platforms.length(); i++) {
		platforms[i].obj.state = STATE::FADEIN;
	}
}

void initiatePlatformMovementBoss() {
	for (uint i = 0; i < bossPlatforms.length(); i++) {
		bossPlatforms[i].obj.state = STATE::FADEIN;
	}
}

void initializeIntro() {
	fioCut::initializeCutscene(@processTickEvents, cutsceneTextsIntro);
	fioCut::createEventFade(DURATION_FADE_TOTAL, DURATION_FADE_BLACKOUT, false, true);
	fioCut::setTickEventsProcessed(true);
	play.noFire = true;
	play.cameraFreeze(INTRO_ROLL_X, INTRO_ROLL_Y, true, true);
	activeCutscene = CUTSCENE_INTRO;
}

void initializeOutro() {
	play.noFire = true;
	play.invisibility = true;
	play.invincibility = 7000; // Should suffice
	isPlayerHiddenAndUnableToMove = true;
	isPlayerRenderedInAir = true;
	fioCut::initializeCutscene(@processTickEvents, cutsceneTextsOutro);
	activeCutscene = CUTSCENE_OUTRO;
}

void initializeBossCutsceneAnimationChain() {
	// Duration, xOrigin, yOrigin, xDestination, yDestination, angle, scaleX, scaleY, animSet, animationId,
	// startingFrame, finalFrame, frameRate, repetitions (optional)
	// REMINDER: DON'T FORGET TO REMOVE THE TRAILING COMMA, SINCE OTHERWISE AS WILL INSERT A NULL HANDLE AFTER THE LAST ACTUAL OBJECT ELEMENT
	
	const array<fioCut::Animation@> animationsBoss = {
		fioCut::Animation(CUTSCENE_SECOND * 44,
				BOSS_X, BOSS_Y,
				BOSS_X, BOSS_Y,
				0, 1, 1, ANIM::BUBBA, 5, 0, 0, 1, 0, false, -1) // Framerate doesn't really matter here but w/e
	};
	fioCut::createAnimationChain(animationsBoss);
}

void initializeIntroAnimationChain() {
	// Duration, xOrigin, yOrigin, xDestination, yDestination, angle, scaleX, scaleY, animSet, animationId,
	// startingFrame, finalFrame, frameRate, repetitions (optional)
	// REMINDER: DON'T FORGET TO REMOVE THE TRAILING COMMA, SINCE OTHERWISE AS WILL INSERT A NULL HANDLE AFTER THE LAST ACTUAL OBJECT ELEMENT
	const array<fioCut::Animation@> animationsIntroRabbit = {
		fioCut::Animation(RABBIT_ROLL_DURATION,
				TILE * 222, TILE * 37,
				TILE * 222, TILE * 37,
				0, 1, 1, playerAnimSet, RABBIT::ROLLING, 0, 7, FRAME_RATE_INTRO_RABBIT),
		fioCut::EmptyAnimation(CUTSCENE_SECOND),
		fioCut::Animation(8,
				TILE * 187, TILE * 61,
				INTRO_X, INTRO_Y,
				0, 1, 1, playerAnimSet, RABBIT::ROLLING, 0, 7, FRAME_RATE_INTRO_RABBIT),
		fioCut::Animation(4,
				INTRO_X, INTRO_Y,
				INTRO_X, INTRO_Y,
				0, 1, 1, playerAnimSet, RABBIT::ROLLING, 0, 7, FRAME_RATE_INTRO_RABBIT),
		fioCut::Animation(RABBIT_STILL_DURATION_INTRO,
				INTRO_X, INTRO_Y,
				INTRO_X, INTRO_Y,
				0, 1, 1, playerAnimSet, character.idleAnimation, character.idleFrame, character.idleFrame, 1, 0, false, -1), // Framerate doesn't really matter here but w/e
		fioCut::Animation(16,
				INTRO_X, INTRO_Y,
				INTRO_X, INTRO_Y,
				0, 1, 1, playerAnimSet, character.digAnimation, character.digFrameStart, character.digFrameEnd, 2, 1, false, -1),
		fioCut::Animation(RABBIT_MIND_STONE_DURATION,
				INTRO_X, INTRO_Y,
				INTRO_X, INTRO_Y,
				0, 1, 1, playerAnimSet, character.withMindStoneAnimation, character.withMindStoneFrame, character.withMindStoneFrame, 1, 0, false, -1),
		fioCut::Animation(16,
				INTRO_X, INTRO_Y,
				INTRO_X, INTRO_Y,
				0, 1, 1, playerAnimSet, character.digAnimation, character.digFrameEnd, character.digFrameStart, 2, 1, true, -1),
		fioCut::Animation(CUTSCENE_SECOND * 5,
				INTRO_X, INTRO_Y,
				INTRO_X, INTRO_Y,
				0, 1, 1, playerAnimSet, character.idleAnimation, character.idleFrame, character.idleFrame, 1, 0, false, -1)
	};
	fioCut::createAnimationChain(animationsIntroRabbit);
}

void initializePlatforms() {
	platforms = array<Platform@>(0);
	
	// The x argument should represent the location of the left edge of the platform
	// and the y argument should represent the top part of the platform
	array<array<Node@>> nodeSets = {
		{
			Node(TILE * 176, TILE * 60 + PLATFORM_OFFSET),
			Node(TILE * 160, TILE * 60 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 160, TILE * 43 + PLATFORM_OFFSET),
			Node(TILE * 160, TILE * 59 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 160, TILE * 42 + PLATFORM_OFFSET),
			Node(TILE * 176, TILE * 42 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 182, TILE * 39 + PLATFORM_OFFSET),
			Node(TILE * 183, TILE * 38 + PLATFORM_OFFSET),
			Node(TILE * 183, TILE * 37 + PLATFORM_OFFSET),
			Node(TILE * 170, TILE * 34 + PLATFORM_OFFSET),
			Node(TILE * 166, TILE * 24 + PLATFORM_OFFSET),
			Node(TILE * 166, TILE * 30 + PLATFORM_OFFSET),
			Node(TILE * 170, TILE * 34 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 145, TILE * 24 + PLATFORM_OFFSET), // 5th
			Node(TILE * 133, TILE * 36 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 142, TILE * 39 + PLATFORM_OFFSET),
			Node(TILE * 130, TILE * 27 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 120, TILE * 49 + PLATFORM_OFFSET),
			Node(TILE * 132, TILE * 37 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 144, TILE * 39 + PLATFORM_OFFSET),
			Node(TILE * 132, TILE * 51 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 83, TILE * 53 + PLATFORM_OFFSET),
			Node(TILE * 83, TILE * 42 + PLATFORM_OFFSET),
			Node(TILE * 77, TILE * 42 + PLATFORM_OFFSET),
			Node(TILE * 77, TILE * 53 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 80, TILE * 40 + PLATFORM_OFFSET), // 10th
			Node(TILE * 80, TILE * 32 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 109, TILE * 59 + PLATFORM_OFFSET),
			Node(TILE * 100, TILE * 65 + PLATFORM_OFFSET),
			Node(TILE * 96, TILE * 65 + PLATFORM_OFFSET),
			Node(TILE * 90, TILE * 56 + PLATFORM_OFFSET),
			Node(TILE * 100, TILE * 65 + PLATFORM_OFFSET),
			Node(TILE * 106, TILE * 65 + PLATFORM_OFFSET),
			Node(TILE * 109, TILE * 60 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 84, TILE * 32 + PLATFORM_OFFSET),
			Node(TILE * 84, TILE * 18 + PLATFORM_OFFSET),
			Node(TILE * 76, TILE * 18 + PLATFORM_OFFSET),
			Node(TILE * 76, TILE * 32 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 76, TILE * 18 + PLATFORM_OFFSET),
			Node(TILE * 76, TILE * 32 + PLATFORM_OFFSET),
			Node(TILE * 84, TILE * 32 + PLATFORM_OFFSET),
			Node(TILE * 84, TILE * 18 + PLATFORM_OFFSET)
		}
	};
	
	array<float> platformSpeeds = {
		2.0,
		2.0,
		2.0,
		2.0,
		2.0, // 5th
		2.0,
		2.0,
		2.0,
		2.0,
		3.0, // 10th
		3.0,
		3.0,
		3.0
	};
	
	array<uint16> platformTileIds = {
		50, 56, 60, 66
	};
	
	for (uint i = 0; i < nodeSets.length(); i++) {
		platforms.insertLast(
				Platform(
						jjObjects[jjAddObject(OBJECT::PINKPLATFORM, nodeSets[i][0].x, nodeSets[i][0].y)],
						nodeSets[i],
						4, // renderOffsetY
						platformSpeeds[i],
						platformTileIds
				)
		);
	}
}

void initializePlatformsAtBoss() {
	bossPlatforms = array<Platform@>(0);
	
	// The x argument should represent the location of the left edge of the platform
	// and the y argument should represent the top part of the platform
	array<array<Node@>> nodeSets = {
		{
			Node(TILE * 31, TILE * 61 + PLATFORM_OFFSET),
			Node(TILE * 31, TILE * 54 + PLATFORM_OFFSET),
			Node(TILE * 31, TILE * 68 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 44, TILE * 61 + PLATFORM_OFFSET),
			Node(TILE * 44, TILE * 68 + PLATFORM_OFFSET),
			Node(TILE * 44, TILE * 54 + PLATFORM_OFFSET)
		}
	};
	
	array<float> platformSpeeds = {
		2.0,
		2.0
	};
	
	array<uint16> platformTileIds = {
		50, 53, 56, 60, 63, 66
	};
	
	for (uint i = 0; i < nodeSets.length(); i++) {
		bossPlatforms.insertLast(
				Platform(
						jjObjects[jjAddObject(OBJECT::PINKPLATFORM, nodeSets[i][0].x, nodeSets[i][0].y)],
						nodeSets[i],
						4, // renderOffsetY
						platformSpeeds[i],
						platformTileIds,
						5 // layerZ = 5, so that other renders are drawn front
				)
		);
	}
}

bool isBubbaDead() {
	return @bubba !is null && bubba.energy <= 0;
}

// Required for each level
bool onCheat(string &in cheat) {
	return fio::handleCheat(cheat, NEXT_LEVEL_FILENAME);
}

// Required for each level
bool onDrawHealth(jjPLAYER@ play, jjCANVAS@ canvas) {
	fioDraw::animateHud();
	fioDraw::drawHud(play, canvas, activeCutscene != CUTSCENE_NONE);
	
	if (activeCutscene != CUTSCENE_NONE) {
		fioCut::drawCutscene(canvas, centeredText, activeCutscene == CUTSCENE_BOSS);
	}
	
	if (playerHeatLevel >= 3) {
		canvas.drawRectangle(0, 0, jjSubscreenWidth, jjSubscreenHeight, 24, SPRITE::BLEND_NORMAL,
				uint8(abs(jjSin(jjGameTicks % 512 * 2) * getHeatWarningIntensityLevel())));
	}
	
	return activeCutscene != CUTSCENE_NONE;
}

void onDrawLayer4(jjPLAYER@ play, jjCANVAS@ canvas) {
	if (activeCutscene != CUTSCENE_NONE) {
		fioCut::renderAnimations(canvas);
		if (isMindStoneVisible) {
			drawMindStoneAtIntro(canvas);
		}
	}
	
	fio::renderCommon(play, canvas);
	drawIceSource(canvas);
	
	if (isCustomBossActivated && !isBubbaDead()) {
		for (uint i = 0; i < MAX_HEAT_LEVEL; i++) {
			canvas.drawSprite(int(play.xPos) - 32 + i * 16, int(play.yPos) - 32, ANIM::AMMO, 13, 1, 0, SPRITE::SINGLECOLOR, 31);
		}
		for (uint i = 0; i < playerHeatLevel; i++) {
			canvas.drawSprite(int(play.xPos) - 32 + i * 16, int(play.yPos) - 32, ANIM::AMMO, 13, 1, 0);
		}
	} else if (isPlayerHeatLevelFadingIn) {
		for (uint i = 0; i < MAX_HEAT_LEVEL; i++) {
			canvas.drawSprite(int(play.xPos) - 32 + i * 16, int(play.yPos) - 32, ANIM::AMMO, 13, 1, 0, SPRITE::BLEND_COLOR, playerHeatLevelFade);
		}
		for (uint i = 0; i < playerHeatLevel; i++) {
			canvas.drawSprite(int(play.xPos) - 32 + i * 16, int(play.yPos) - 32, ANIM::AMMO, 13, 1, 0, SPRITE::BLEND_NORMAL, playerHeatLevelFade);
		}
	}
	
	if (isPlayerRenderedInAir) {
		fioDraw::drawFrameAtPlayerPos(play, canvas, fio::getAnimSetForPlayer(play), RABBIT::FALL, 0);
	}
}

bool onDrawLives(jjPLAYER@ play, jjCANVAS@ canvas) {
	return true;
}

bool onDrawScore(jjPLAYER@ play, jjCANVAS@ canvas) {
	return activeCutscene != CUTSCENE_NONE;
}

void onFunction0() {
	play.lighting = LIGHTING_TWILIGHT;
	if (activeCutscene == CUTSCENE_NONE && !checkpoints[0].isReached()) {
		checkpoints[0].setReached();
	}
}

void onFunction1() {
	fioDraw::doShowText(0);
}

void onFunction2() {
	play.lighting = LIGHTING_TWILIGHT;
	if (!checkpoints[1].isReached()) {
		checkpoints[1].setReached();
	}
}

void onFunction3() {
	if (!checkpoints[2].isReached()) {
		checkpoints[2].setReached();
	}
}

void onFunction4() {
	play.lighting = LIGHTING_TWILIGHT;
}

void onFunction5() {
	fioDraw::doShowText(1);
}

void onFunction6() {
	isPlayerUnableToMove = true;
	play.warpToID(1);
}

void onFunction7() {
	play.lighting = LIGHTING_STANDARD;
	if (!wasCutsceneWatchedTillTheEndBoss) {
		play.cameraFreeze(TILE * 39, TILE * 60, true, false);
		fioCut::initializeCutscene(@processTickEvents, cutsceneTextsBoss);
		activeCutscene = CUTSCENE_BOSS;
		initializeBossCutsceneAnimationChain();
	} else {
		endCutsceneBoss();
	}
}

void onFunction8() {
	// hurt() instead of kill(), because kill() won't display the actual roast animation
	play.hurt(7, true);
}

void onFunction9() {
	if (play.ammo[3] < 99) {
		play.ammo[3] = 99;
	}
	if (!hasIceSourceTextBeenShown) {
		fioDraw::doShowText(2);
		hasIceSourceTextBeenShown = true;
	}
}

void onFunction10() {
	play.warpToID(1);
}

void onLevelBegin() {
	@character = fio::getCharacterWithMindStoneForPlayer(play, true);
	initializeIntro();
	initializePlatforms();
	initializePlatformsAtBoss();
}

// Required for each level
void onLevelLoad() {
	initializeGlobals(fio4bCheckpoints, 0);
	fioDraw::initializeDrawing(texts, array<string>(0), false);
	playerAnimSet = fio::getAnimSetForPlayer(jjLocalPlayers[0]);
	
	if (jjDifficulty >= 2) {
		playerHeatLevelIncreaseInterval = 210;
	}
	
	mindstoneCommunicationTileIds = array<uint16>(5);
	mindstoneCommunicationTileIds[0] = 960;
	mindstoneCommunicationTileIds[1] = 969;
	mindstoneCommunicationTileIds[2] = 970;
	mindstoneCommunicationTileIds[3] = 979;
	mindstoneCommunicationTileIds[4] = 980;
}

// Required for each level
void onLevelReload() {
	MLLE::ReapplyPalette();
	reloadGlobals();
	fioDraw::initializeDrawing(texts, array<string>(0), false);
	initializePlatforms();
	
	if (fio::handleLevelReload()) {
		activeCutscene = CUTSCENE_NONE;
	} else {
		initializeIntro();
	}
	
	if (hasPlayerSteppedOnFirstPlatform) {
		initiatePlatformMovement();
	}
	
	isCustomBossActivated = false;
	isPlayerHeatLevelFadingIn = false;
	playerHeatLevelFade = 0;
	jjEnabledASFunctions[7] = true;
	playerHeatLevel = 1;
	playerHeatLevelElapsed = 0;
	play.jumpStrength = -10;
	jjObjectPresets[OBJECT::ICEBULLET].behavior = BEHAVIOR::BULLET;
	jjObjectPresets[OBJECT::ICEBULLETPU].behavior = BEHAVIOR::BULLET;
	initializePlatformsAtBoss(); // Reset boss platforms back to the original states
	
	if (isBubbaDead()) {
		fio::handleLevelCycle(NEXT_LEVEL_FILENAME, true); // Just in case you mess up the outro by jjk or something :-)
	}
}

// Required for each level
void onMain() {
	fio::controlPressedKeys();
	fioDraw::controlHud();
}

// Required for each level
void onPlayer(jjPLAYER@ play) {
	fio::handlePlayer(play);
	
	if (activeCutscene != CUTSCENE_NONE) {
		fioCut::run();
		if (!fioCut::isTickEventsProcessed()) {
			processTickEvents(play);
			fioCut::setTickEventsProcessed(true);
		}
	}
	if (!hasPlayerSteppedOnFirstPlatform && play.platform > 0) {
		hasPlayerSteppedOnFirstPlatform = true;
		initiatePlatformMovement();
	}
	
	if (isCustomBossActivated && isBubbaDead()) {
		bubba.obj.particlePixelExplosion(1);
		bubba.obj.state = STATE::KILL;
		bubba.obj.delete();
		fio::rewardPoints(getPointRewardByDifficulty());
		isCustomBossActivated = false;
		initializeOutro();
	} else if (isCustomBossActivated && !isBubbaDead()) {
		controlBubba();
		controlPlayerHeatLevel(play);
	} else if (isPlayerHeatLevelFadingIn && playerHeatLevelFade < 255) {
		playerHeatLevelFade++;
	}
	
	if (isPlayerRenderedInAir) {
		play.xPos = TILE * 39;
		play.yPos = TILE * 60;
	}
}

void onPlayerInput(jjPLAYER@ play) {
	fio::controlArmoryInput(play);
	fio::controlPlayerInput(play, activeCutscene != CUTSCENE_NONE);
	if (activeCutscene != CUTSCENE_NONE) {
		fioCut::controlPlayerInput(play);
		if (fioCut::isCutsceneSkipInitiatedAfterDelay(play)) {
			fioCut::setCutsceneSkipInitiated();
			if (activeCutscene == CUTSCENE_INTRO) {
				endCutsceneIntro();
			} else if (activeCutscene == CUTSCENE_BOSS) {
				endCutsceneBoss();
			} else if (activeCutscene == CUTSCENE_OUTRO) {
				fioUtils::releasePlayer();
				isPlayerRenderedInAir = false;
				fio::handleLevelCycle(NEXT_LEVEL_FILENAME, true, true);
				// Don't set activeCutscene to CUTSCENE_NONE so that the cutscene engine can process the level cycle event properly
			}
		}
	}
}

void onRoast(jjPLAYER@ victim, jjPLAYER@ killer) {
	fio::saveTriggerStates();
	asPlay.savePlayerProperties(play);
}

void processCutsceneBoss(jjPLAYER@ play) {
	switch(uint(fioCut::getElapsedCutscene())) {
		case 8:
			fioCut::startTextSliding();
			isPlayerHiddenAndUnableToMove = true;
			staticPlayerRenderState = STATE_STATIC_PLAYER_RENDER_IDLE;
			break;
		case CUTSCENE_SECOND * 33:
			isPlayerHeatLevelFadingIn = true;
			break;
		case CUTSCENE_SECOND * 44 - 20:
			fio::increaseCutscenesWatchedIfFastForwardWasNotUsed(fioCut::wasFastForwardUsed);
			wasCutsceneWatchedTillTheEndBoss = true;
			endCutsceneBoss();
			break;
	}
}

void processCutsceneIntro(jjPLAYER@ play) {
	switch(uint(fioCut::getElapsedCutscene())) {
		case CUTSCENE_SECOND * 1:
			fioCut::startTextSliding();
			isPlayerHiddenAndUnableToMove = true;
			break;
		case CUTSCENE_SECOND * 4:
			play.xPos = TILE * 221;
			play.yPos = TILE * 55;
			break;
		case CUTSCENE_SECOND * 8:
			initializeIntroAnimationChain();
			break;
		case CUTSCENE_SECOND * 24:
			play.xPos = INTRO_X;
			play.yPos = INTRO_Y;
			jjSamplePriority(SOUND::COMMON_TELPORT2);
			fioCut::createEventCameraScroll(25, TILE * 191, INTRO_Y,
					INTRO_X, INTRO_Y);
			break;
		case CUTSCENE_SECOND * 35 + 11:
			isMindStoneVisible = true;
			fioCut::isMindstoneCommunicationRendered = true;
			break;
		case CUTSCENE_SECOND * 43 + 6:
			isMindStoneVisible = false;
			fioCut::isMindstoneCommunicationRendered = false;
			break;
		case CUTSCENE_SECOND * 47 - 3:
			// Get player out of the ball mode in time
			play.ballTime = 0;
			break;
		case CUTSCENE_SECOND * 48:
			fio::increaseCutscenesWatchedIfFastForwardWasNotUsed(fioCut::wasFastForwardUsed);
			endCutsceneIntro();
			break;
	}
}

void processCutsceneOutro(jjPLAYER@ play) {
	switch(uint(fioCut::getElapsedCutscene())) {
		case 1:
			fioCut::startTextSliding();
			break;
		case CUTSCENE_SECOND * 4:
			fioUtils::releasePlayer();
			isPlayerRenderedInAir = false;
			fio::increaseCutscenesWatchedIfFastForwardWasNotUsed(fioCut::wasFastForwardUsed);
			fio::handleLevelCycle(NEXT_LEVEL_FILENAME, true);
			break;
	}
}

void processTickEvents(jjPLAYER@ play) {
	switch (activeCutscene) {
		case CUTSCENE_INTRO:
			processCutsceneIntro(play);
			break;
		case CUTSCENE_BOSS:
			processCutsceneBoss(play);
			break;
		case CUTSCENE_OUTRO:
			processCutsceneOutro(play);
			break;
	}
}