Downloads containing Fio4_a.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_a-MLLE-Data-1.j2l" ///@MLLE-Generated
#pragma require "wbrg.j2t" ///@MLLE-Generated
#pragma require "Fio4_a.j2l" ///@MLLE-Generated
#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_INTERLUDE };

// Requires ANIM::WITCH to be loaded or a Witch event present in the level
// Level-based implementation with custom texts displayed
class PossessedRock : jjBEHAVIORINTERFACE {

	float xOrg;
	float yOrg;
	float transitionElapsed = 0;
	float transitionLength = 70;
	float transitionSpeedX;
	float transitionSpeedY;

	PossessedRock(jjOBJ@ obj) {
		xOrg = obj.xOrg;
		yOrg = obj.yOrg;
		obj.behavior = this;
		obj.deactivates = false;
		obj.playerHandling = HANDLING::SPECIAL;
		obj.scriptedCollisions = true;
	}
	
	void onBehave(jjOBJ@ obj) override {
		if (obj.freeze > 0 && !hasRockFreezeTextBeenShown) {
			fioDraw::doShowText(3);
			hasRockFreezeTextBeenShown = true;
		}
		switch (obj.playerHandling) {
			case HANDLING::SPECIAL:
				obj.behave(BEHAVIOR::BIGOBJECT);
				break;
			case HANDLING::SPECIALDONE:
				obj.behave(BEHAVIOR::EVA);
				if (transitionElapsed > 0) {
					transitionElapsed--;
					moveTowardsOrigin(obj);
				} else {
					obj.freeze = 0;
					obj.playerHandling = HANDLING::SPECIAL;
					// Just to double check
					obj.xPos = xOrg;
					obj.yPos = yOrg;
				}
				break;
		}
	}
	
	void onDraw(jjOBJ@ obj) {
		if (obj.playerHandling == HANDLING::SPECIALDONE) {
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::BLEND_DISSOLVE, 128);
			jjDrawRotatedSprite(obj.xPos + 20, obj.yPos - 36, ANIM::WITCH, 1, 55, 512, 2, 2, SPRITE::BLEND_DISSOLVE, 128);
		} else if (obj.freeze <= 0) {
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::TINTED, 24);
			jjDrawRotatedSprite(obj.xPos + 20, obj.yPos - 36, ANIM::WITCH, 1, 55, 512, 2, 2);
		}
	}
	
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		// Only when hit by a special move or a buttstomp
		if (@bullet !is null) {
			bullet.state = STATE::EXPLODE;
		} else if (@player !is null && (force == -1 || force == 1)) {
			if (!hasRockReturnTextBeenShown) {
				fioDraw::doShowText(2);
				hasRockReturnTextBeenShown = true;
			}
			
			// Set the object's platform behavior to 0 to ensure that the game no longer thinks that the player is standing on the rock when the behavior changes
			// (and walk on air, because it seems that the platform property is used to determine that)
			obj.clearPlatform();
			obj.playerHandling = HANDLING::SPECIALDONE;
			transitionElapsed = transitionLength;
			float xDistance = xOrg - obj.xPos;
			float yDistance = yOrg - obj.yPos;
			transitionSpeedX = xDistance / transitionLength;
			transitionSpeedY = yDistance / transitionLength;
		}
		return true;
	}
	
	// Required to preserve the object stacking behavior on top of each other with custom behavior classes
	bool onIsSolid(jjOBJ@ obj) {
		return true;
	}
	
	private void moveTowardsOrigin(jjOBJ@ obj) {
		obj.xPos += transitionSpeedX;
		obj.yPos += transitionSpeedY;
	}
}

const float DURATION_FADE_TOTAL = CUTSCENE_SECOND * 4;
const float DURATION_FADE_BLACKOUT = CUTSCENE_SECOND * 2;
const float DURATION_INTRO_SCROLL_FAST = CUTSCENE_SECOND * 3;
const float DURATION_INTRO_SCROLL_SLOW = CUTSCENE_SECOND * 2;
const float INTERLUDE_X = TILE * 216;
const float INTERLUDE_Y = TILE * 150;
const float INTRO_END_Y = TILE * 101 + 8;
const float INTRO_MID_Y = TILE * 91;
const float INTRO_START_Y = TILE * 11;
const float INTRO_X = TILE * 103;
const float PLATFORM_OFFSET = 26;
const float RABBIT_DIZZY_DURATION = CUTSCENE_SECOND * 2;
const float RABBIT_MIND_STONE_DURATION = CUTSCENE_SECOND * 8;
const float RABBIT_ROLL_DURATION = CUTSCENE_SECOND * 6;
const float RABBIT_STILL_DURATION_INTRO = CUTSCENE_SECOND * 5 - 22;
const float RABBIT_STILL_DURATION_INTERLUDE = CUTSCENE_SECOND * 7 - 22;
const float RABBIT_WARP_Y = TILE * 97;

const string NEXT_LEVEL_FILENAME = "Fio4_x.j2l";

const array<ArmoryItem@> ARMORY_ITEMS = {
	ArmoryItem(0, "||||Bouncer Power up@+ 20 ammo", 20, 52, ANIM::PICKUPS, 61, 0, @fio::sellArmoryItemBouncerPU),
	ArmoryItem(1, "||||Toaster Power up@+20 ammo", 20, 48, ANIM::PICKUPS, 65, 0, @fio::sellArmoryItemToasterPU),
	ArmoryItem(2, "||||Pepperspray Power up@+20 ammo", 20, 48, ANIM::PICKUPS, 66, 0, @fio::sellArmoryItemPeppersprayPU),
	ArmoryItem(3, "||||Freezer Power up@+20 ammo", 12, 48, ANIM::PICKUPS, 62, 0, @fio::sellArmoryItemFreezerPU),
	ArmoryItem(4, "||||+15 Seeker ammo", 25, 48, ANIM::AMMO, 37, 1, @fio::sellArmoryItemSeekerAmmo),
	ArmoryItem(5, "", 15, 44, ANIM::PICKUPS, 72, 5, @fio::sellArmoryItemInvincibility, @fio::canBuyInvincibility), // Text updated later when currentGameSession has been loaded
	ArmoryItem(6, "", 10, 56, ANIM::PICKUPS, 21, 0, @fio::sellArmoryItemPocketCarrot, @fio::canBuyPocketCarrot) // Text updated later when currentGameSession has been loaded
};

uint8 activeCutscene = uint8(CUTSCENE_NONE);

bool isMindStoneVisible = false;
bool hasFirstTriggerCrateBeenDestroyed = false;
bool hasIceSourceTextBeenShown = false;
bool hasInterludeCutsceneBeenShown = false;
bool hasRockFreezeTextBeenShown = false;
bool hasRockReturnTextBeenShown = false;

ANIM::Set playerAnimSet;

CharacterWithMindStone@ character;

array<Platform@> platforms;

array<Checkpoint@> fio4aCheckpoints = {
	Checkpoint(0, INTRO_X, INTRO_END_Y),
	Checkpoint(1, INTERLUDE_X, INTERLUDE_Y),
	Checkpoint(2, TILE * 234, TILE * 209),
	Checkpoint(3, TILE * 72, TILE * 193)
};

array<string> questTexts = {
	fio::getQuestText(10, 15, 10000),
	fio::getQuestTextComplete(10000),
	fio::getQuestTextPerfect(5000)
};

array<string> texts = {
	"|Five more gates? Where are the switches and how to get up...Wait what, floating platforms???",
	"||Greetings mortal! Meet us, the possessed rocks! If you want us to get out of your way, just give us a punch and we will obey.",
	"||Ack! Back to my rest!",
	"||ARGH! THAT'S COLD!",
	"|What's this burrow about? Doesn't seem like I can go this way without getting hurt.", // 4
	"|I guess my only way ahead is to get over that ledge up there somehow.",
	"|This seems like a good source of ice! I wonder what can I use it for. Hmm...",
	"|I can't even see the bottom of the pit below. I...must...not...fall now...",
	"|Hmm...I sense mystic energies. Perhaps I could use the energies for my destination.",
	"|I wonder if that wall is destructible..."
};

array<string> cutsceneTextsIntro = {
	"||||As the statue of curses cracks down into pieces, you start to suffer a familiar kind of pain, knowing what is happening yet unaware of where you will end.",
	"||||After a short while that felt like an eternity in pain, your surroundings are quite different once again.",
	"", // Empty for focus on visuality
	"", // Empty for focus on visuality
	"|Now what is this place? Did I get all the curses from the statue on me?",
	"||||You take your mindstone into your hand, trying to get a contact to Nicholas, but you cannot hear anything.",
	"|I'll have to get moving and get an understanding of this place. Hopefully I can get a contact to Nicholas later..."
};

array<string> cutsceneTextsInterlude = {
	"||||After being warped deeper into the darkness, the mindstone that you received from Nicholas starts to glow, to your surprise...",
	"||||You carefully focus your attention on the mindstone before moving forward. You finally hear the voice of Nicholas in your head again, although weaker than before...",
	"|||Where are you? I sense a weakness in the energies guarding my cell, but I cannot break free with my current powers just yet.",
	"|I have no idea. I brought down the statue of curses as you requested. Then suddenly I got myself summoned into a dark place once again.",
	"|I wouldn't know how to describe this place anyway, it's quite dark, but also chillier than the places I've visited previously.",
	"|I recall you telling me that you would summon me back to your place after tearing down the statue of curses. Weren't you the one who summoned me here?",
	"|||I'm suspecting that whoever destroys the statue of curses, will get themselves summoned to the realm of darkness by the spirits right away...",
	"|||I think I have overestimated my powers. Even with the statue of curses destroyed, I cannot help you right now. You'll have to find your way to me.",
	"|||Only then can we both find a way out of this place, but that may require obtaining my powers back in some way.",
	"|||It may be that a part of my powers are possessed by someone who rules this prison region. You may have to find that someone.",
	"|Easier said than done! But I won't give up. I will find my way there. Just hold on a bit longer!"
};

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

void drawMindStoneAtPlayerPos(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_END_Y);
	fioCut::isMindstoneCommunicationRendered = false;
	isMindStoneVisible = false;
	play.lighting = LIGHTING_TWILIGHT;
	activeCutscene = CUTSCENE_NONE;
	checkpoints[0].setReached();
	fioUtils::releasePlayer();
}

void endCutsceneInterlude() {
	fioCut::endCutscene(INTERLUDE_X, INTERLUDE_Y);
	fioCut::isMindstoneCommunicationRendered = false;
	isMindStoneVisible = false;
	play.lighting = LIGHTING_TWILIGHT;
	activeCutscene = CUTSCENE_NONE;
	fioUtils::releasePlayer();
}

int getRemainingGateTriggerCrateAmount() {
	int count = 0;
	for (int i = 2; i < 7; ++i) {
		if (!jjTriggers[i]) {
			count++;
		}
	}
	return count;
}

void initiatePlatformMovement() {
	for (uint i = 0; i < platforms.length(); i++) {
		platforms[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_X, INTRO_START_Y, true, true);
	activeCutscene = CUTSCENE_INTRO;
}

void initializeInterludeAnimationChain() {
	// 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@> animationsInterludeRabbit = {
		fioCut::Animation(RABBIT_STILL_DURATION_INTERLUDE,
				INTERLUDE_X, INTERLUDE_Y,
				INTERLUDE_X, INTERLUDE_Y,
				0, 1, 1, playerAnimSet, character.idleAnimation, character.idleFrame, character.idleFrame, 1), // Framerate doesn't really matter here but w/e
		fioCut::Animation(16,
				INTERLUDE_X, INTERLUDE_Y,
				INTERLUDE_X, INTERLUDE_Y,
				0, 1, 1, playerAnimSet, character.digAnimation, character.digFrameStart, character.digFrameEnd, 2, 1),
		fioCut::Animation(CUTSCENE_SECOND * 76 - 3,
				INTERLUDE_X, INTERLUDE_Y,
				INTERLUDE_X, INTERLUDE_Y,
				0, 1, 1, playerAnimSet, character.withMindStoneAnimation, character.withMindStoneFrame, character.withMindStoneFrame, 1),
		fioCut::Animation(16,
				INTERLUDE_X, INTERLUDE_Y,
				INTERLUDE_X, INTERLUDE_Y,
				0, 1, 1, playerAnimSet, character.digAnimation, character.digFrameEnd, character.digFrameStart, 2, 1, true),
		fioCut::Animation(CUTSCENE_SECOND,
				INTERLUDE_X, INTERLUDE_Y,
				INTERLUDE_X, INTERLUDE_Y,
				0, 1, 1, playerAnimSet, character.idleAnimation, character.idleFrame, character.idleFrame, 1)
	};
	fioCut::createAnimationChain(animationsInterludeRabbit);
}

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 * 104, TILE * 8,
				TILE * 104, TILE * 8,
				0, 1, 1, playerAnimSet, RABBIT::ROLLING, 0, 7, FRAME_RATE_INTRO_RABBIT),
		fioCut::EmptyAnimation(CUTSCENE_SECOND * 8),
		fioCut::Animation(10,
				INTRO_X, RABBIT_WARP_Y,
				INTRO_X, RABBIT_WARP_Y,
				0, 1, 1, playerAnimSet, RABBIT::TELEPORTFALL, 0, 7, 2, 1),
		fioCut::Animation(15,
				INTRO_X, RABBIT_WARP_Y,
				INTRO_X, INTRO_END_Y,
				0, 1, 1, playerAnimSet, RABBIT::TELEPORTFALLING, 0, 4, 2),
		fioCut::Animation(10,
				INTRO_X, INTRO_END_Y,
				INTRO_X, INTRO_END_Y,
				0, 1, 1, playerAnimSet, RABBIT::BUTTSTOMPLAND, 0, 6, 2, 1),
		fioCut::Animation(RABBIT_DIZZY_DURATION,
				INTRO_X, INTRO_END_Y,
				INTRO_X, INTRO_END_Y,
				0, 1, 1, playerAnimSet, RABBIT::STONED, 0, playerAnimSet == ANIM::LORI ? 11 : 7, FRAME_RATE_INTRO_RABBIT),
		fioCut::Animation(RABBIT_STILL_DURATION_INTRO,
				INTRO_X, INTRO_END_Y,
				INTRO_X, INTRO_END_Y,
				0, 1, 1, playerAnimSet, character.idleAnimation, character.idleFrame, character.idleFrame, 1), // Framerate doesn't really matter here but w/e
		fioCut::Animation(16,
				INTRO_X, INTRO_END_Y,
				INTRO_X, INTRO_END_Y,
				0, 1, 1, playerAnimSet, character.digAnimation, character.digFrameStart, character.digFrameEnd, 2, 1),
		fioCut::Animation(CUTSCENE_SECOND * 13 - 3,
				INTRO_X, INTRO_END_Y,
				INTRO_X, INTRO_END_Y,
				0, 1, 1, playerAnimSet, character.withMindStoneAnimation, character.withMindStoneFrame, character.withMindStoneFrame, 1),
		fioCut::Animation(16,
				INTRO_X, INTRO_END_Y,
				INTRO_X, INTRO_END_Y,
				0, 1, 1, playerAnimSet, character.digAnimation, character.digFrameEnd, character.digFrameStart, 2, 1, true),
		fioCut::Animation(CUTSCENE_SECOND,
				INTRO_X, INTRO_END_Y,
				INTRO_X, INTRO_END_Y,
				0, 1, 1, playerAnimSet, character.idleAnimation, character.idleFrame, character.idleFrame, 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 * 102, TILE * 88 + PLATFORM_OFFSET),
			Node(TILE * 102, TILE * 99 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 103, TILE * 63 + PLATFORM_OFFSET),
			Node(TILE * 96, TILE * 59 + PLATFORM_OFFSET),
			Node(TILE * 96, TILE * 68 + PLATFORM_OFFSET),
			Node(TILE * 97, TILE * 81 + PLATFORM_OFFSET),
			Node(TILE * 106, TILE * 84 + PLATFORM_OFFSET),
			Node(TILE * 117, TILE * 81 + PLATFORM_OFFSET),
			Node(TILE * 117, TILE * 59 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 164, TILE * 61 + PLATFORM_OFFSET),
			Node(TILE * 164, TILE * 74 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 188, TILE * 77 + PLATFORM_OFFSET),
			Node(TILE * 188, TILE * 85 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 176, TILE * 24 + PLATFORM_OFFSET), // 5th
			Node(TILE * 182, TILE * 21 + PLATFORM_OFFSET),
			Node(TILE * 194, TILE * 23 + PLATFORM_OFFSET),
			Node(TILE * 194, TILE * 32 + PLATFORM_OFFSET),
			Node(TILE * 188, TILE * 34 + PLATFORM_OFFSET),
			Node(TILE * 178, TILE * 36 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 141, TILE * 102 + PLATFORM_OFFSET),
			Node(TILE * 141, TILE * 113 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 180, TILE * 209 + PLATFORM_OFFSET),
			Node(TILE * 174, TILE * 215 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 166, TILE * 211 + PLATFORM_OFFSET),
			Node(TILE * 172, TILE * 217 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 165, TILE * 224 + PLATFORM_OFFSET),
			Node(TILE * 171, TILE * 218 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 163, TILE * 224 + PLATFORM_OFFSET), // 10th
			Node(TILE * 157, TILE * 218 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 150, TILE * 206 + PLATFORM_OFFSET),
			Node(TILE * 150, TILE * 218 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 140, TILE * 218 + PLATFORM_OFFSET),
			Node(TILE * 148, TILE * 218 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 103, TILE * 208 + PLATFORM_OFFSET),
			Node(TILE * 103, TILE * 220 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 39, TILE * 205 + PLATFORM_OFFSET),
			Node(TILE * 39, TILE * 196 + PLATFORM_OFFSET)
		},
		{
			Node(TILE * 28, TILE * 195 + PLATFORM_OFFSET), // 15th
			Node(TILE * 20, TILE * 195 + PLATFORM_OFFSET),
			Node(TILE * 20, TILE * 184 + PLATFORM_OFFSET),
			Node(TILE * 28, TILE * 184 + PLATFORM_OFFSET)
		}
	};
	
	array<float> platformSpeeds = {
		1.5,
		2.5,
		2.0,
		1.0,
		2.5, // 5th
		1.5,
		1.5,
		1.5,
		1.5,
		1.5, // 10th
		2.0,
		1.5,
		2.0,
		2.5,
		2.5 // 15th
	};
	
	array<uint16> tileIds = {
		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],
						tileIds
				)
		);
	}
}

void initializePossessedRocks() {
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 192, TILE * 115.5)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 198, TILE * 115.5)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 220, TILE * 102.5)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 228, TILE * 102.5)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 236, TILE * 102.5)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 236, TILE * 96.5)]);
	if (play.charOrig == CHAR::LORI) {
		PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 240, TILE * 102.5)]);
	}
	if (jjDifficulty < 1) { // Easy
		PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 232, TILE * 123)]);
	}
}

// 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 (shouldShowRemainingGateTriggerCrates()) {
		canvas.drawString(jjSubscreenWidth - 48, 40, "" + getRemainingGateTriggerCrateAmount() + "x", STRING::MEDIUM, centeredText);
		canvas.drawResizedSprite(jjSubscreenWidth - 16, 40, ANIM::PICKUPS, 52, 0, 0.75, 0.75);
	}
	
	if (activeCutscene != CUTSCENE_NONE) {
		fioCut::drawCutscene(canvas, centeredText);
	}
	
	if (isPlayerInArmory) {
		fioDraw::drawArmoryInterface(canvas);
	}
	
	return activeCutscene != CUTSCENE_NONE;
}

void onDrawLayer4(jjPLAYER@ play, jjCANVAS@ canvas) {
	if (activeCutscene != CUTSCENE_NONE) {
		fioCut::renderAnimations(canvas);
		if (isMindStoneVisible) {
			drawMindStoneAtPlayerPos(canvas);
		}
	}
	
	fio::renderCommon(play, canvas);
	fioDraw::drawArmoryAtPos(canvas, TILE * 79.5, TILE * 172.5); // Offset with +0.5 xTiles and +0.5 yTiles
	drawIceSource(canvas);
}

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

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

void onFunction0() {
	fioDraw::doShowOptionalQuest(0);
}

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

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

void onFunction3() {
	fioDraw::doShowText(4);
}

void onFunction4() {
	fioDraw::doShowText(5);
}

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

void onFunction6() {
	play.warpToTile(216, 150);
	if (!checkpoints[1].isReached()) {
		checkpoints[1].setReached();
	}
}

void onFunction7() {
	play.lighting = LIGHTING_TWILIGHT;
	if (!hasInterludeCutsceneBeenShown) {
		isPlayerUnableToMove = true;
		fioCut::initializeCutscene(@processTickEvents, cutsceneTextsInterlude);
		activeCutscene = CUTSCENE_INTERLUDE;
		hasInterludeCutsceneBeenShown = true;
	}
}

void onFunction8(bool show) {
	if (show) {
		play.noFire = true;
		isPlayerInArmory = true;
	} else {
		play.noFire = false;
		isPlayerInArmory = false;
	}
}

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

void onFunction10() {
	fioDraw::doShowText(7);
}

void onFunction11() {
	fioDraw::doShowText(8);
}

void onFunction12() {
	fio::handleLevelCycle(NEXT_LEVEL_FILENAME, true);
}

void onFunction13() {
	fioDraw::doShowText(9);
}

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

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

// Required for each level
void onLevelLoad() {
	initializeGlobals(fio4aCheckpoints, 10, 10000, 5000);
	fioDraw::initializeDrawing(texts, questTexts, true);
	playerAnimSet = fio::getAnimSetForPlayer(jjLocalPlayers[0]);
	
	// Re-assignment via static declaration
	armoryItems = ARMORY_ITEMS;
	
	// Global indice
	armoryItemIndexInvincibility = 5;
	armoryItemIndexPocketCarrot = 6;
	
	armoryItems[armoryItemIndexInvincibility].text = fio::getInvincibilityItemText();
	armoryItems[armoryItemIndexPocketCarrot].text = fio::getPocketCarrotItemText();
	
	mindstoneCommunicationTileIds = array<uint16>(5);
	mindstoneCommunicationTileIds[0] = 960;
	mindstoneCommunicationTileIds[1] = 969;
	mindstoneCommunicationTileIds[2] = 970;
	mindstoneCommunicationTileIds[3] = 979;
	mindstoneCommunicationTileIds[4] = 980;
	
	jjAnimSets[ANIM::WITCH].load();
}

// Required for each level
void onLevelReload() {
	reloadGlobals();
	fioDraw::initializeDrawing(texts, questTexts, true);
	
	if (fio::handleLevelReload()) {
		activeCutscene = CUTSCENE_NONE;
	} else {
		initializeIntro();
	}
	
	initializePlatforms();
	initializePossessedRocks();
	
	if (jjTriggers[1]) {
		initiatePlatformMovement();
	}
}

// Required for each level
void onMain() {
	fio::controlPressedKeys();
	fioDraw::controlHud();
	
	if (!hasFirstTriggerCrateBeenDestroyed && jjTriggers[1]) {
		hasFirstTriggerCrateBeenDestroyed = true;
		fioDraw::doShowText(0);
		initiatePlatformMovement();
	}
}

// Required for each level
void onPlayer(jjPLAYER@ play) {
	fio::handlePlayer(play);
	fio::controlQuest();
	
	if (activeCutscene != CUTSCENE_NONE) {
		fioCut::run();
		if (!fioCut::isTickEventsProcessed()) {
			processTickEvents(play);
			fioCut::setTickEventsProcessed(true);
		}
	}
}

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_INTERLUDE) {
				endCutsceneInterlude();
			}
		}
	}
}

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

void processCutsceneIntro(jjPLAYER@ play) {
	switch(uint(fioCut::getElapsedCutscene())) {
		case CUTSCENE_SECOND * 1:
			fioCut::startTextSliding();
			isPlayerHiddenAndUnableToMove = true;
			break;
		case CUTSCENE_SECOND * 4:
			play.xPos = TILE * 117;
			play.yPos = TILE * 12;
			break;
		case CUTSCENE_SECOND * 5:
			initializeIntroAnimationChain();
			break;
		case CUTSCENE_SECOND * 14:
			fioCut::createEventCameraScroll(DURATION_INTRO_SCROLL_FAST, INTRO_X, INTRO_START_Y,
					INTRO_X, INTRO_MID_Y);
			play.xPos = TILE * 91;
			play.yPos = TILE * 109;
			setObjectPresetsDeactivates(false);
			break;
		case CUTSCENE_SECOND * 17:
			fioCut::createEventCameraScroll(DURATION_INTRO_SCROLL_SLOW, INTRO_X, INTRO_MID_Y,
					INTRO_X, INTRO_END_Y);
			break;
		case CUTSCENE_SECOND * 19:
			setObjectPresetsDeactivates(true);
			play.xPos = INTRO_X;
			play.yPos = INTRO_END_Y;
			jjSamplePriority(SOUND::COMMON_TELPORT2);
			break;
		case CUTSCENE_SECOND * 27:
			isMindStoneVisible = true;
			fioCut::isMindstoneCommunicationRendered = true;
			break;
		case CUTSCENE_SECOND * 40 - 3:
			// Get player out of the ball mode in time
			play.ballTime = 0;
			isMindStoneVisible = false;
			fioCut::isMindstoneCommunicationRendered = false;
			break;
		case CUTSCENE_SECOND * 41:
			fio::increaseCutscenesWatchedIfFastForwardWasNotUsed(fioCut::wasFastForwardUsed);
			endCutsceneIntro();
			break;
	}
}

void processCutsceneInterlude(jjPLAYER@ play) {
	switch(uint(fioCut::getElapsedCutscene())) {
		case 8:
			fioCut::startTextSliding();
			isPlayerHiddenAndUnableToMove = true;
			// isPlayerRenderedIdle = true;
			initializeInterludeAnimationChain();
			break;
		case CUTSCENE_SECOND * 8 - 32:
			isMindStoneVisible = true;
			fioCut::isMindstoneCommunicationRendered = true;
			break;
		case CUTSCENE_SECOND * 83:
			// Get player out of the ball mode in time
			play.ballTime = 0;
			break;
		case CUTSCENE_SECOND * 84 - 32:
			isMindStoneVisible = false;
			fioCut::isMindstoneCommunicationRendered = false;
			break;
		case CUTSCENE_SECOND * 84 - 6:
			fio::increaseCutscenesWatchedIfFastForwardWasNotUsed(fioCut::wasFastForwardUsed);
			endCutsceneInterlude();
			break;
	}
}

void processTickEvents(jjPLAYER@ play) {
	switch (activeCutscene) {
		case CUTSCENE_INTRO:
			processCutsceneIntro(play);
			break;
		case CUTSCENE_INTERLUDE:
			processCutsceneInterlude(play);
			break;
	}
}

bool shouldShowRemainingGateTriggerCrates() {
	if (!jjTriggers[1]) {
		return false;
	}
	
	for (int i = 2; i < 7; ++i) {
		if (!jjTriggers[i]) {
			return true;
		}
	}
	
	return false;
}

// For displaying objects during cutscene when the player isn't close to the camera view
void setObjectPresetsDeactivates(bool deactivates) {
	jjObjectPresets[OBJECT::BAT].deactivates = deactivates;
	jjObjectPresets[OBJECT::CARROTCRATE].deactivates = deactivates;
	jjObjectPresets[OBJECT::DEMON].deactivates = deactivates;
	jjObjectPresets[OBJECT::GOLDCOIN].deactivates = deactivates;
	jjObjectPresets[OBJECT::GRAPES].deactivates = deactivates;
	jjObjectPresets[OBJECT::GUNCRATE].deactivates = deactivates;
	jjObjectPresets[OBJECT::RAPIER].deactivates = deactivates;
	jjObjectPresets[OBJECT::SANDWICH].deactivates = deactivates;
	jjObjectPresets[OBJECT::TRIGGERCRATE].deactivates = deactivates;
}