Downloads containing Fio5_c.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 "Fio5_c-MLLE-Data-1.j2l" ///@MLLE-Generated
#pragma require "Inferno1.j2t" ///@MLLE-Generated
#pragma require "Fio5_c.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_INTERLUDE, CUTSCENE_BOSS };

class Lift : jjBEHAVIORINTERFACE {

	// Stored obj handle for state management outside the class
	jjOBJ@ obj;
	float renderOffsetY;
	bool isLeftWallClosed = false;
	
	Lift(jjOBJ@ obj) {
		@this.obj = obj;
		obj.behavior = this;
		obj.deactivates = false;
		obj.isFreezable = false;
		obj.state = STATE::WAIT;
		isLeftWallClosed = false;
		renderOffsetY = 0;
	}
	
	void onBehave(jjOBJ@ obj) override {
		// NOTE: MAKE SURE TO CALL THE bePlatform WITH THE OLD VALUES INSTEAD OF MODIFYING xPos AND yPos "on the fly"
		// Otherwise the player will fall through the platform easily, etc.
		float xOld = obj.xPos;
		float yOld = obj.yPos;
		
		switch (obj.state) {
			case STATE::FADEIN:
				{
					obj.yPos += fioCut::getPaceNormalized();
				}
				break;
			case STATE::FADEOUT:
				{
					if (obj.yPos < INTERLUDE_END_Y) {
						obj.yPos += fioCut::getPaceNormalized() * 8;
					} else {
						fio::increaseCutscenesWatchedIfFastForwardWasNotUsed(fioCut::wasFastForwardUsed);
						endCutsceneInterlude();
						obj.state = STATE::KILL;
						return;
					}
				}
				break;
			case STATE::KILL:
				return;
		}
		obj.bePlatform(xOld, yOld, TILE * 7, TILE);
	}
	
	void onDraw(jjOBJ@ obj) {
		// Draw the platform graphics starting from the bottom (where the platform part is)
		if (obj.state == STATE::FADEIN) {
			renderOffsetY = fioCut::getPaceNormalized() * -1 - 2;
		} else if (obj.state == STATE::FADEOUT) {
			renderOffsetY = fioCut::getPaceNormalized() * -8 - 2;
		} else {
			renderOffsetY = -2;
		}
		
		// LAYER 4
		jjDrawTile(obj.xPos, obj.yPos + renderOffsetY, 2);
		jjDrawTile(obj.xPos, obj.yPos - TILE * 6  + renderOffsetY, 2);
		jjDrawTile(obj.xPos + TILE * 6, obj.yPos - TILE * 6  + renderOffsetY, 2); // Top right corner
		
		for (uint i = 1; i < 7; i++) {
			jjDrawTile(obj.xPos + TILE * i, obj.yPos + renderOffsetY, i % 3 == 0 ? 2 : 4);
			if (isLeftWallClosed && i < 6) {
				jjDrawTile(obj.xPos, obj.yPos - TILE * i + renderOffsetY, i % 3 == 0 ? 2 : 4);
			}
		}
		
		for (uint i = 1; i < 6; i++) {
			jjDrawTile(obj.xPos + TILE * 6 - TILE * i, obj.yPos - TILE * 6 + renderOffsetY, i == 3 ? 2 : 4);
			jjDrawTile(obj.xPos + TILE * 6, obj.yPos - TILE * 6 + TILE * i + renderOffsetY, i == 3 ? 2 : 4);
		}
		
		// LAYER 3
		jjDrawTile(obj.xPos, obj.yPos + renderOffsetY, 91, TILE::ALLQUADRANTS, 3);
		jjDrawTile(obj.xPos, obj.yPos - TILE * 6 + renderOffsetY, 91, TILE::ALLQUADRANTS, 3);
		jjDrawTile(obj.xPos + TILE * 6, obj.yPos - TILE * 6 + renderOffsetY, 91, TILE::ALLQUADRANTS, 3); // Top right corner
		
		for (uint i = 1; i < 7; i++) {
			jjDrawTile(obj.xPos + TILE * i, obj.yPos + renderOffsetY, i % 3 == 0 ? 91 : 90, TILE::ALLQUADRANTS, 3);
			jjDrawTile(obj.xPos, obj.yPos - TILE * i + renderOffsetY, i % 3 == 0 ? 91 : 81, TILE::ALLQUADRANTS, 3);
		}
		
		for (uint i = 1; i < 6; i++) {
			jjDrawTile(obj.xPos + TILE * 6 - TILE * i, obj.yPos - TILE * 6 + renderOffsetY, i == 3 ? 91 : 90, TILE::ALLQUADRANTS, 3);
			jjDrawTile(obj.xPos + TILE * 6, obj.yPos - TILE * 6 + TILE * i + renderOffsetY, i == 3 ? 91 : 81, TILE::ALLQUADRANTS, 3);
		}
		
		// SPRITE LAYER CHAINS
		for (uint i = 1; i < 10; i++) {
			for (uint j = 0; j < 7; j++) {
				if (j % 3 == 0) {
					jjDrawTile(obj.xPos + TILE * j, obj.yPos - TILE * 16 + TILE * i + renderOffsetY, 110);
				}
			}
		}
	}
	
	void addParticleTileExplosions() {
		// Convert pos to tile with bit shift to the right by 5
		uint16 xTile = uint16(obj.xPos) >> 5;
		uint16 yTile = uint16(obj.yPos) >> 5;
		jjAddParticleTileExplosion(xTile, yTile, 2, true);
		
		jjAddParticleTileExplosion(xTile, yTile - 6, 2, true);
		jjAddParticleTileExplosion(xTile + 6, yTile - 6, 2, true); // Top right corner
		
		for (uint i = 1; i < 7; i++) {
			jjAddParticleTileExplosion(xTile + i, yTile, i % 3 == 0 ? 2 : 4, true);
			jjAddParticleTileExplosion(xTile, yTile - i, i % 3 == 0 ? 2 : 4, true);
		}
		
		for (uint i = 1; i < 6; i++) {
			jjAddParticleTileExplosion(xTile + 6 - i, yTile - 6, i == 3 ? 2 : 4, true);
			jjAddParticleTileExplosion(xTile + 6, yTile - 6 + i, i == 3 ? 2 : 4, true);
		}
		
		// SPRITE LAYER CHAINS
		for (uint i = 1; i < 10; i++) {
			for (uint j = 0; j < 7; j++) {
				if (j % 3 == 0) {
					jjAddParticleTileExplosion(xTile + j, yTile - 16 + i, 110, true);
				}
			}
		}
	}
}

class SummoningFire {
	int x;
	int y;
	int elapsed;
	SummoningFire(int x, int y) {
		this.x = x;
		this.y = y;
		elapsed = 0;
	}
	bool process(jjPLAYER@ play) {
		// Offset player location by 16 pixels
		if (abs(play.xPos - float(x + 16)) < 32 && abs(play.yPos - float(y)) < 32) {
			play.hurt(1);
		}
		if (elapsed < SUMMONING_FIRE_DURATION) {
			elapsed++;
			return false;
		}
		return true;
	}
	void draw(jjCANVAS@ canvas) {
		canvas.drawTile(x, y, 71 + TILE::ANIMATED);
	}
}

class SummoningPickup {
	int x;
	int y;
	OBJECT::Object pickupId;
	
	SummoningPickup(int x, int y, OBJECT::Object pickupId) {
		this.x = x;
		this.y = y;
		this.pickupId = pickupId;
	}
}

class SummoningRock {
	int x;
	int y;
	SummoningRock(int x, int y) {
		this.x = x + SUMMONING_ROCK_OFFSET;
		this.y = y + SUMMONING_ROCK_OFFSET;
	}
	bool process() {
		if (!jjMaskedPixel(x, y + SUMMONING_ROCK_OFFSET)) {
			y += SUMMONING_ROCK_SPEED_Y;
			return false;
		}
		int frame = int(jjAnimations[jjAnimSets[ANIM::ROCK].firstAnim].firstFrame);
		jjAddParticlePixelExplosion(float(this.x), float(this.y), frame, 0, 0);
		jjSample(float(this.x), float(this.y), SOUND::DEVILDEVAN_DRAGONFIRE);
		return true;
	}
	void draw(jjCANVAS@ canvas) {
		canvas.drawRotatedSprite(x, y, ANIM::ROCK, 0, 0, jjGameTicks % 1024 * 24, 0.5, 0.5, SPRITE::TINTED, 24);
	}
}

const float BOSS_ARENA_START_X = TILE * 120.25;
const float BOSS_ARENA_START_Y = TILE * 226;
const float DURATION_FADE_TOTAL = CUTSCENE_SECOND * 1;
const float DURATION_FADE_BLACKOUT = CUTSCENE_SECOND * 0.5;
const float FRAME_RATE_INTERLUDE_RABBIT = 2;
const float INTERLUDE_END_X = TILE * 39.5;
const float INTERLUDE_END_Y = TILE * 198;
const float PLATFORM_OFFSET_X = -7;
const float PLATFORM_OFFSET_Y = 17;
const float RABBIT_FRIGHTENED_DURATION = CUTSCENE_SECOND * 18;
const float RABBIT_STILL_DURATION = CUTSCENE_SECOND * 15 - 15;

const int BOSS_ARENA_WALL_MOTION_END = 221;
const int LIFT_WALL_MOTION_END = 44;
const int FLOOR_MOTION_START_X = 63;
const int OUTRO_WALL_MOTION_END = 221;
const int SUMMONING_FIRE_DURATION = 140;
const int SUMMONING_FLOOR_FIRST_Y = TILE * 229;
const int SUMMONING_RAIN_INTERVAL = 700;
const int SUMMONING_PICKUPS_INTERVAL = 1050;
const int SUMMONING_ROCK_SPEED_Y = 8;
const int SUMMONING_ROCK_OFFSET = 16;
const int WALL_MOTION_INTERVAL = 5;

const uint SUMMONING_RANGE_MAX_Y = 203;
const uint SUMMONING_RANGE_MIN_Y = 187;

const uint8 MAX_ENEMIES_ON_SUMMONING_FLOOR = 30; // Can go a bit over this too, when there are multiple enemies summoned at once
const uint8 MAX_PICKUPS_ON_SUMMONING_FLOOR = 30; // Can go a bit over this too, when there are multiple pickups summoned at once

array<uint> SUMMONING_RANGES_X = { 116, 123, 129, 135, 142 };

const string NEXT_LEVEL_FILENAME = "Fio6_a.j2l";

const array<Checkpoint@> FIO5_C_CHECKPOINTS = {
	Checkpoint(0, INTERLUDE_END_X, INTERLUDE_END_Y),
	Checkpoint(1, TILE * 111, TILE * 220)
};

const array<OBJECT::Object> SUMMONING_ENEMIES = {
	OBJECT::DEMON,
	OBJECT::DOGGYDOGG,
	OBJECT::DRAGON,
	OBJECT::HELMUT,
	OBJECT::SKELETON,
	OBJECT::SUCKER
};

const array<OBJECT::Object> SUMMONING_PICKUPS = {
	OBJECT::BOUNCERAMMO3,
	OBJECT::SEEKERAMMO3,
	OBJECT::RFAMMO3,
	OBJECT::TOASTERAMMO3,
	OBJECT::GUN8AMMO3,
	OBJECT::FASTFIRE
};

bool hasBossArenaBeenReached = false;
bool hasLiftMiddleXBeenReached = false;
bool hasFallingPickupsTextBeenShown = false;
bool hasPlatformTextBeenShown = false;
bool isBaronOfHellRendered = true;
bool isBossArenaWallMotionActive = false;
bool isFallingPickupsTextDelayed = false;
bool isLiftCollapseDelaying = false;
bool isLiftExploding = false;
bool isLiftWallMotionActive = false;
bool isOutroActive = false;
bool isOutroWallMotionActive = false;
bool isPlayerWaitingToBeWarpedAway = false;
bool wasCutsceneWatchedTillTheEndBoss = false;

int bossArenaWallMotionOffsetY = 210;
int bossBattleElapsed = 0;
int fallingPickupsElapsed = 0;
int liftCollapseDelayElapsed = 0;
int liftExplosionElapsed = 0;
int liftWallMotionOffsetY = 39;
int outroElapsed = 0;
int outroWallMotionOffsetY = 226;
int playerWarpDelayElapsed = 0;

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

ANIM::Set playerAnimSet;

BossFromHell@ baronOfHell;
CharacterWithMindStone@ character;
Lift@ lift;

array<string> cutsceneTextsBoss = {
	"||Congratulations on passing the trial! For now you are worthy enough to burn with me!",
	"|Look, I am just searching for a lost soul and we want to get out of here as quickly as possible. Just let me pass through and we'll be long gone before you know it.",
	"||Hah! Unfortunately it does not work that way! No one leaves this place without battling me first! And no one has made it out alive yet!",
	"||Prepare to face the wrath of me and my minions!!!"
};

array<string> cutsceneTextsInterlude = {
	"|||Free me! Quickly before they sacrifice my soul!",
	"|Woah! Now that was the sound of Nicholas in my head again for sure! Can you hear me Nicholas?",
	"|...",
	"||So you made it this far...",
	"|And now it's that creepy voice again...This is getting really scary! What's next?",
	"|AAAAAAAAAAAAA..."
};

array<string> texts = {
	"|Duh! No one around. Guess I'm just becoming insane in this heck of a place.",
	"|Some kind of an...elevator?",
	"|Ouch my back! That was the roughest elevator landing ever!",
	"|What? A huge arsenal like this? Waiting just for me? And where are all the bad guys now?",
	"|||I'm down here! You must defeat him at all costs! I will support you with my last powers!", // 4th
	"|I wonder what awaits me down there. I guess my only choice is to find it out! I'm coming Nicholas!",
	"|These pickups must be Nicholas'es doing. I gotta make them count!",
	"||WAAAAAAAAAAAAAAAAAAAARRRRRRGGGGGH!",
	"|Nicholas? Is that really you in there?",
	"|||Yes! And now it's time for us to get the hell out of here! Finally I can feel my powers increasing! Here we go!"
};

array<jjOBJ@> summonedEnemies;

array<Platform@> platforms;
array<SummoningFire@> summoningFires;
array<SummoningRock@> summoningRocks;

void clearSummonedEnemies() {
	for (uint i = 0; i < summonedEnemies.length(); i++) {
		if (summonedEnemies[i].isActive && summonedEnemies[i].state != STATE::KILL) {
			jjOBJ@ enemy = summonedEnemies[i];
			enemy.particlePixelExplosion(1); // 1 = fire explosion
			enemy.state = STATE::KILL;
		}
	}
	summonedEnemies = array<jjOBJ@>(0);
}

void clearSummoningFires() {
	summoningFires = array<SummoningFire@>(0);
}

void clearSummoningRocks() {
	for (uint i = 0; i < summoningRocks.length(); i++) {
		SummoningRock@ summoningRock = summoningRocks[i];
		int frame = int(jjAnimations[jjAnimSets[ANIM::ROCK].firstAnim].firstFrame);
		jjAddParticlePixelExplosion(float(summoningRock.x), float(summoningRock.y), frame, 0, 0);
	}
	summoningRocks = array<SummoningRock@>(0);
}

void controlSky(jjPLAYER@ play) {
	if (bossBattleElapsed % SUMMONING_RAIN_INTERVAL == 0 && !isSummoningFloorFullFromEnemies()) {
		summonEnemiesFromTheSky();
	}
	
	if (bossBattleElapsed % SUMMONING_PICKUPS_INTERVAL == 0 && !isSummoningFloorFullFromPickups()) {
		if (!hasFallingPickupsTextBeenShown && !isFallingPickupsTextDelayed) {
			fallingPickupsElapsed = 0;
			isFallingPickupsTextDelayed = true;
		}
		summonPickupsFromTheSky();
	}
}

void drawSummoningFires(jjCANVAS@ canvas) {
	for (uint i = 0; i < summoningFires.length(); i++) {
		summoningFires[i].draw(canvas);
	}
}

void drawSummoningRocks(jjCANVAS@ canvas) {
	for (uint i = 0; i < summoningRocks.length(); i++) {
		summoningRocks[i].draw(canvas);
	}
}

void endCutsceneBoss() {
	fioCut::endCutscene(BOSS_ARENA_START_X, BOSS_ARENA_START_Y);
	activeCutscene = CUTSCENE_NONE;
	staticPlayerRenderState = STATE_STATIC_PLAYER_RENDER_OFF;
	fioCut::clearAnimationChains();
	fioUtils::releasePlayer();
	@baronOfHell = BossFromHell(fio::getActiveObjectFromLevel(OBJECT::BILSY), getBossHealthByDifficulty());
	baronOfHell.obj.state = STATE::DELAYEDSTART;
	isCustomBossActivated = true;
	isBaronOfHellRendered = false;
	jjMusicLoad(BOSS_THEME_FILENAME, false, true);
	fioDraw::textIndex = -1;
	bossBattleElapsed = 0;
	jjGenerateSettableTileArea(4, 115, 227, 2, 1);
	jjTileSet(4, 115, 227, 0);
	jjTileSet(4, 116, 227, 0);
	fio::preserveAmmoForBossBattle();
}

void endCutsceneInterlude() {
	// Make sure to call clearPlatform() before trying to delete the lift object and move player away
	if (lift.obj.yPos < INTERLUDE_END_Y) {
		lift.obj.yPos = INTERLUDE_END_Y;
	}
	fioCut::endCutscene(INTERLUDE_END_X, INTERLUDE_END_Y);
	play.lighting = LIGHTING_TWILIGHT;
	activeCutscene = CUTSCENE_NONE;
	fioUtils::releasePlayer();
	fioDraw::doShowText(2);
	
	staticPlayerRenderState = STATE_STATIC_PLAYER_RENDER_OFF;
	liftCollapseDelayElapsed = 0;
	isLiftCollapseDelaying = true;
	liftExplosionElapsed = 0;
	liftExplosionFrame = 0;
	isLiftExploding = true;
	jjSample(INTERLUDE_END_X - TILE, INTERLUDE_END_Y - TILE, SOUND::COMMON_BENZIN1);
	jjSample(INTERLUDE_END_X + TILE, INTERLUDE_END_Y - TILE, SOUND::COMMON_BENZIN1);
	jjSample(INTERLUDE_END_X + TILE, INTERLUDE_END_Y + TILE, SOUND::COMMON_BENZIN1);
	jjSample(INTERLUDE_END_X - TILE, INTERLUDE_END_Y + TILE, SOUND::COMMON_BENZIN1);
	jjTriggers[1] = true;
	
	play.hurt(0, true);
}

int getBossHealthByDifficulty() {
	if (jjDifficulty >= 3) return 1337; // PrupelJazz L33t mode xP
	if (jjDifficulty == 2) return 999;
	if (jjDifficulty == 1) return 666; // GRMBL
	return 300;
}

int getPointRewardByDifficulty() {
	if (jjDifficulty >= 3) return 66600; 
	if (jjDifficulty == 2) return 6660;
	if (jjDifficulty == 1) return 666; // Ezoons Anno Domini
	return 666;
}

RABBIT::Anim getRabbitAnimForFrightenedState() {
	if (play.charCurr == CHAR::LORI || play.charCurr == CHAR::SPAZ) {
		return RABBIT::DIVE;
	}
	// Default Jazz
	return RABBIT::ENDOFLEVEL;
}

int getRandomUnusedLocationFromRange(uint range, array<uint> usedLocations) {
	uint random = jjRandom() % range;
	while (usedLocations.find(random) >= 0) {
		random = jjRandom() % range;
	}
	return random;
}

OBJECT::Object getRandomUnusedPickup(array<OBJECT::Object> usedPickups) {
	// For safety
	if (usedPickups.length() >= SUMMONING_PICKUPS.length()) {
		return SUMMONING_PICKUPS[jjRandom() % SUMMONING_PICKUPS.length()];
	}

	uint random = jjRandom() % SUMMONING_PICKUPS.length();
	
	while (usedPickups.find(SUMMONING_PICKUPS[random]) >= 0) {
		random = jjRandom() % SUMMONING_PICKUPS.length();
	}
	return SUMMONING_PICKUPS[random];
}

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

void initializeBaronOfHellAnimationChain() {
	// 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(15,
				TILE * 126.5, TILE * 226,
				TILE * 126.5, TILE * 226,
				0, 1, 1, ANIM::BILSBOSS, 1, 15, 0, 1, 1, true)
	};
	
	fioCut::createAnimationChain(animationsInterludeRabbit);
}

void initializeLift() {
	@lift = Lift(jjObjects[jjAddObject(OBJECT::SONICPLATFORM, TILE * 36, TILE * 44 + 2)]);
}

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 * 20 + PLATFORM_OFFSET_X, TILE * 66 + PLATFORM_OFFSET_Y),
			Node(TILE * 33 + PLATFORM_OFFSET_X, TILE * 66 + PLATFORM_OFFSET_Y),
			Node(TILE * 33 + PLATFORM_OFFSET_X, TILE * 120 + PLATFORM_OFFSET_Y),
			Node(TILE * 20 + PLATFORM_OFFSET_X, TILE * 120 + PLATFORM_OFFSET_Y)
		},
		{
			Node(TILE * 33 + PLATFORM_OFFSET_X, TILE * 120 + PLATFORM_OFFSET_Y),
			Node(TILE * 20 + PLATFORM_OFFSET_X, TILE * 120 + PLATFORM_OFFSET_Y),
			Node(TILE * 20 + PLATFORM_OFFSET_X, TILE * 66 + PLATFORM_OFFSET_Y),
			Node(TILE * 33 + PLATFORM_OFFSET_X, TILE * 66 + PLATFORM_OFFSET_Y)
		},
		{
			Node(TILE * 38 + PLATFORM_OFFSET_X, TILE * 122 + PLATFORM_OFFSET_Y),
			Node(TILE * 51 + PLATFORM_OFFSET_X, TILE * 122 + PLATFORM_OFFSET_Y)
		},
		{
			Node(TILE * 84 + PLATFORM_OFFSET_X, TILE * 123 + PLATFORM_OFFSET_Y),
			Node(TILE * 97 + PLATFORM_OFFSET_X, TILE * 123 + PLATFORM_OFFSET_Y)
		},
		{
			Node(TILE * 101 + PLATFORM_OFFSET_X, TILE * 123 + PLATFORM_OFFSET_Y),
			Node(TILE * 105 + PLATFORM_OFFSET_X, TILE * 126 + PLATFORM_OFFSET_Y),
			Node(TILE * 109 + PLATFORM_OFFSET_X, TILE * 123 + PLATFORM_OFFSET_Y),
			Node(TILE * 104 + PLATFORM_OFFSET_X, TILE * 126 + PLATFORM_OFFSET_Y)
		},
		{
			Node(TILE * 117 + PLATFORM_OFFSET_X, TILE * 115 + PLATFORM_OFFSET_Y),
			Node(TILE * 114 + PLATFORM_OFFSET_X, TILE * 119 + PLATFORM_OFFSET_Y),
			Node(TILE * 109 + PLATFORM_OFFSET_X, TILE * 123 + PLATFORM_OFFSET_Y),
			Node(TILE * 113 + PLATFORM_OFFSET_X, TILE * 119 + PLATFORM_OFFSET_Y)
		},
		{
			Node(TILE * 121 + PLATFORM_OFFSET_X, TILE * 115 + PLATFORM_OFFSET_Y),
			Node(TILE * 141 + PLATFORM_OFFSET_X, TILE * 115 + PLATFORM_OFFSET_Y)
		},
		{
			Node(TILE * 145 + PLATFORM_OFFSET_X, TILE * 115 + PLATFORM_OFFSET_Y),
			Node(TILE * 161 + PLATFORM_OFFSET_X, TILE * 113 + PLATFORM_OFFSET_Y),
			Node(TILE * 162 + PLATFORM_OFFSET_X, TILE * 115 + PLATFORM_OFFSET_Y),
			Node(TILE * 147 + PLATFORM_OFFSET_X, TILE * 117 + PLATFORM_OFFSET_Y)
		}
	};
	
	array<float> platformSpeeds = {
		2.5,
		2.5,
		2.5,
		2.5,
		2.5,
		2.5,
		3.0,
		3.0
	};
	
	array<uint16> tileIds = {
		10, 13
	};
}

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

bool isObjOfTypeSummoningEnemy(jjOBJ@ obj) {
	return SUMMONING_ENEMIES.find(OBJECT::Object(obj.eventID)) >= 0;
}

bool isObjOfTypeSummoningPickup(jjOBJ@ obj) {
	return SUMMONING_PICKUPS.find(OBJECT::Object(obj.eventID)) >= 0;
}

bool isSummoningFloorFullFromEnemies() {
	uint8 enemyCountOnSummoningFloor = 0;
	for (int i = 1; i < jjObjectCount; ++i) {
		jjOBJ@ obj = jjObjects[i];
		if (isObjOfTypeSummoningEnemy(obj)
				&& obj.xPos > TILE * 114
				&& obj.xPos < TILE * 144
				&& obj.yPos > TILE * 225
				&& obj.yPos < TILE * 230) {
			++enemyCountOnSummoningFloor;
		}
	}
	return enemyCountOnSummoningFloor >= MAX_ENEMIES_ON_SUMMONING_FLOOR;
}

bool isSummoningFloorFullFromPickups() {
	uint8 pickupCountOnSummoningFloor = 0;
	for (int i = 1; i < jjObjectCount; ++i) {
		jjOBJ@ obj = jjObjects[i];
		if (isObjOfTypeSummoningPickup(obj)
				&& obj.xPos > TILE * 114
				&& obj.xPos < TILE * 144
				&& obj.yPos > TILE * 225
				&& obj.yPos < TILE * 230) {
			++pickupCountOnSummoningFloor;
		}
	}
	return pickupCountOnSummoningFloor >= MAX_PICKUPS_ON_SUMMONING_FLOOR;
}

void nullifyPointBonusesFromEnemiesAndObjects() {
	jjObjectPresets[OBJECT::DEMON].points = 0;
	jjObjectPresets[OBJECT::DOGGYDOGG].points = 0;
	jjObjectPresets[OBJECT::DRAGON].points = 0;
	jjObjectPresets[OBJECT::HELMUT].points = 0;
	jjObjectPresets[OBJECT::SKELETON].points = 0;
	jjObjectPresets[OBJECT::SUCKER].points = 0;
	
	jjObjectPresets[OBJECT::CARROT].points = 0;
	
	jjObjectPresets[OBJECT::BOUNCERAMMO3].points = 0;
	jjObjectPresets[OBJECT::FASTFIRE].points = 0;
	jjObjectPresets[OBJECT::GUN8AMMO3].points = 0;
	jjObjectPresets[OBJECT::ICEAMMO3].points = 0;
	jjObjectPresets[OBJECT::RFAMMO3].points = 0;
	jjObjectPresets[OBJECT::SEEKERAMMO3].points = 0;
	jjObjectPresets[OBJECT::TOASTERAMMO3].points = 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);
	}
	
	return activeCutscene != CUTSCENE_NONE;
}

void onDrawLayer4(jjPLAYER@ play, jjCANVAS@ canvas) {
	if (activeCutscene != CUTSCENE_NONE) {
		fioCut::renderAnimations(canvas);
	}
	
	if (isBaronOfHellRendered) {
		canvas.drawSprite(TILE * 126.5, TILE * 226, ANIM::BILSBOSS, 1, 16);
	}
	
	if (isLiftExploding) {
		if (liftExplosionElapsed < 114) {
			canvas.drawResizedSprite(int(INTERLUDE_END_X), int(INTERLUDE_END_Y), ANIM::AMMO, 5, liftExplosionFrame, 6, 6);
			++liftExplosionElapsed;
			
			if (liftExplosionElapsed % 10 == 0) {
				++liftExplosionFrame;
			}
			
		} else {
			isLiftExploding = false;
		}
	}
	
	fio::renderCommon(play, canvas);
	drawSummoningFires(canvas);
	drawSummoningRocks(canvas);
	
	jjDrawResizedSprite(TILE * 159.5, TILE * 224.5, ANIM::AMMO, 10, 0, 2, 2);
}

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

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

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

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

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

void onFunction3() {
	forceMoveRight = true;
	if (!checkpoints[0].isReached()) {
		checkpoints[0].setReached();
	}
}

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

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

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

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

void onFunction8() {
	bossArenaWallMotionOffsetY = 210;
	forceMoveRight = true;
	fioCut::pace = fioCut::NORMAL_PACE; // Dirty hack for forceMoveRight when a cutscene may not be initialized, if the cutscene was watched till the end
}

void onFunction9() {
	forceMoveRight = true;
	fioDraw::doShowText(8);
}

void onFunction10() {
	forceMoveRight = false;
	isPlayerHiddenAndUnableToMove = true;
	staticPlayerRenderState = STATE_STATIC_PLAYER_RENDER_FOCUSED;
	fioDraw::doShowText(9);
	outroElapsed = 0;
	isOutroActive = true;
}

// Required for each level
void onLevelBegin() {
	@character = fio::getCharacterWithMindStoneForPlayer(play);
	initializeLift();
	initializePlatforms();
	initiatePlatformMovement();
}

// Required for each level
void onLevelLoad() {
	// NOTE TO SELF: Make sure to initialize globals (custom object preset behaviors) in onLevelLoad instead of onLevelBegin, since apparently
	// some events/objects may already be placed in the level prior to running that hook, but initializing things here should ensure that all objects get
	// the custom behavior presets set
	initializeGlobals(FIO5_C_CHECKPOINTS);
	fioDraw::initializeDrawing(texts, array<string>(0), false);
	playerAnimSet = fio::getAnimSetForPlayer(jjLocalPlayers[0]);
	
	jjGenerateSettableTileArea(4, 36, 38, 7, 7);
	jjGenerateSettableTileArea(4, 114, 210, 1, 11);
	jjGenerateSettableTileArea(4, 144, 222, 1, 5);
	
	jjAnimSets[ANIM::ROCK].load();
	
	jjObjectPresets[OBJECT::CARROT].deactivates = false;
	for (uint i = 0; i < SUMMONING_PICKUPS.length(); i++) {
		jjObjectPresets[SUMMONING_PICKUPS[i]].deactivates = false;
	}
}

// Required for each level
void onLevelReload() {
	MLLE::ReapplyPalette();
	reloadGlobals();
	fioDraw::initializeDrawing(texts, array<string>(0), false);
	
	initializePlatforms();
	initiatePlatformMovement();
	
	fioUtils::releasePlayer();
	bossArenaWallMotionOffsetY = 210;
	bossBattleElapsed = 0;
	fallingPickupsElapsed = 0;
	outroWallMotionOffsetY = 226;
	
	hasBossArenaBeenReached = false;
	isBaronOfHellRendered = true;
	isFallingPickupsTextDelayed = false;
	isCustomBossActivated = false;
	
	summonedEnemies = array<jjOBJ@>(0);
	summoningFires = array<SummoningFire@>(0);
	summoningRocks = array<SummoningRock@>(0);
	
	for (;bossArenaWallMotionOffsetY < BOSS_ARENA_WALL_MOTION_END; ++bossArenaWallMotionOffsetY) {
		jjTileSet(4, 114, bossArenaWallMotionOffsetY, 0);
	}
	
	// Not resetting outro wall, since otherwise player won't be able to re-trigger it anymore :-)
	
	jjGenerateSettableTileArea(4, 114, 210, 1, 11);
	jjGenerateSettableTileArea(4, 144, 222, 1, 5);
	
	jjGenerateSettableTileArea(4, 115, 227, 2, 1);
	jjTileSet(4, 115, 227, 4);
	jjTileSet(4, 116, 227, 4);
	
	if (!fio::handleLevelReload()) {
		jjGenerateSettableTileArea(4, 36, 39, 7, 7);
		
		for (int liftWallMotionOffsetY = 39; liftWallMotionOffsetY < LIFT_WALL_MOTION_END; ++liftWallMotionOffsetY) {
			jjTileSet(4, 36, liftWallMotionOffsetY, liftWallMotionOffsetY == 41 ? 2 : 4);
		}
	}
}

// Required for each level
void onMain() {
	fio::controlPressedKeys();
	fioDraw::controlHud();
	
	processSummonedEnemies();
	processSummoningFires();
	processSummoningRocks();
	
	if (isBossArenaWallMotionActive) {
		if (jjGameTicks % WALL_MOTION_INTERVAL == 0 && bossArenaWallMotionOffsetY < BOSS_ARENA_WALL_MOTION_END) {
			jjTileSet(4, 114, bossArenaWallMotionOffsetY, bossArenaWallMotionOffsetY == 215 ? 2 : 4);
			++bossArenaWallMotionOffsetY;
		} else if (bossArenaWallMotionOffsetY >= BOSS_ARENA_WALL_MOTION_END) {
			isBossArenaWallMotionActive = false;
		}
	}
	
	if (isOutroWallMotionActive) {
		if (jjGameTicks % WALL_MOTION_INTERVAL == 0 && outroWallMotionOffsetY > OUTRO_WALL_MOTION_END) {
			jjTileSet(4, 144, outroWallMotionOffsetY, 0);
			--outroWallMotionOffsetY;
		} else if (outroWallMotionOffsetY <= OUTRO_WALL_MOTION_END) {
			isOutroWallMotionActive = false;
		}
	}
	
	// An artificial delay to ensure that the player is properly relocated into the place where the particle tile explosions are bound to happen
	// Otherwise they won't render to the screen
	if (isLiftCollapseDelaying) {
		if (liftCollapseDelayElapsed < 10) {
			++liftCollapseDelayElapsed;
		} else {
			isLiftCollapseDelaying = false;
			lift.addParticleTileExplosions();
			lift.obj.clearPlatform();
			lift.obj.delete();
			@lift = null;
		}
	}
	
	if (isLiftWallMotionActive) {
		if (jjGameTicks % WALL_MOTION_INTERVAL == 0 && liftWallMotionOffsetY < LIFT_WALL_MOTION_END) {
			jjTileSet(4, 36, liftWallMotionOffsetY, liftWallMotionOffsetY == 41 ? 2 : 4);
			liftWallMotionOffsetY++;
		} else if (liftWallMotionOffsetY >= LIFT_WALL_MOTION_END) {
			isLiftWallMotionActive = false;
			lift.isLeftWallClosed = true;
			
			// Clear the solid wall again as it is now a part of the lift graphics...
			for (int liftWallMotionOffsetY = 39; liftWallMotionOffsetY < LIFT_WALL_MOTION_END; ++liftWallMotionOffsetY) {
				jjTileSet(4, 36, liftWallMotionOffsetY, 0);
			}
		}
	}
	
	if (isPlayerWaitingToBeWarpedAway) {
		if (playerWarpDelayElapsed < 105) {
			playerWarpDelayElapsed++;
		} else {
			isPlayerWaitingToBeWarpedAway = false;
			play.warpToTile(63, 49);
		}
	}
	
	if (!hasLiftMiddleXBeenReached && play.xPos >= TILE * 39.5) {
		hasLiftMiddleXBeenReached = true;
		forceMoveRight = false;
		isLiftWallMotionActive = true;
		isPlayerHiddenAndUnableToMove = true;
		fioCut::initializeCutscene(@processTickEvents, cutsceneTextsInterlude);
		activeCutscene = CUTSCENE_INTERLUDE;
		staticPlayerRenderState = STATE_STATIC_PLAYER_RENDER_IDLE;
	}
	
	if (!hasBossArenaBeenReached && play.xPos >= BOSS_ARENA_START_X) {
		hasBossArenaBeenReached = true;
		forceMoveRight = false;
		isBossArenaWallMotionActive = true;
		if (!wasCutsceneWatchedTillTheEndBoss) {
			isPlayerHiddenAndUnableToMove = true;
			fioCut::initializeCutscene(@processTickEvents, cutsceneTextsBoss);
			activeCutscene = CUTSCENE_BOSS;
			staticPlayerRenderState = STATE_STATIC_PLAYER_RENDER_IDLE;
		} else {
			endCutsceneBoss();
		}
	}
	
	if (isCustomBossActivated && isBaronOfHellDead()) {
		baronOfHell.obj.particlePixelExplosion(1);
		baronOfHell.obj.state = STATE::KILL;
		baronOfHell.obj.delete();
		fio::rewardPoints(getPointRewardByDifficulty());
		isCustomBossActivated = false;
		play.invincibility = 140;
		clearSummoningFires();
		clearSummoningRocks();
		clearSummonedEnemies();
		outroWallMotionOffsetY = 226;
		isOutroWallMotionActive = true;
		fioDraw::doShowText(7);
	} else if (isCustomBossActivated && !isBaronOfHellDead()) {
		++bossBattleElapsed; // Incrementing before controlSky will ensure that modulo conditions won't trigger at bossBattleElapsed = 0
		controlSky(play);
	}
	
	if (isFallingPickupsTextDelayed && fallingPickupsElapsed < 280) {
		++fallingPickupsElapsed;
	} else if (isFallingPickupsTextDelayed) {
		fioDraw::doShowText(6);
		isFallingPickupsTextDelayed = false;
		hasFallingPickupsTextBeenShown = true;
	}
	
	if (isOutroActive) {
		if (outroElapsed == 420) {
			play.lighting = 255;
			isPlayerHiddenAndUnableToMove = false;
			staticPlayerRenderState = STATE_STATIC_PLAYER_RENDER_OFF;
			isPlayerUnableToMove = true;
			fioUtils::releasePlayer();
		}
		if (outroElapsed < 490) {
			++outroElapsed;
		} else {
			isOutroActive = false;
			fio::handleLevelCycle(NEXT_LEVEL_FILENAME, true);
		}
	}
}

// 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);
		}
	}
}

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_BOSS) {
				endCutsceneBoss();
			} else if (activeCutscene == CUTSCENE_INTERLUDE) {
				endCutsceneInterlude();
			}
		}
	}
}

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

void processCutsceneBoss(jjPLAYER@ play) {
	switch(uint(fioCut::getElapsedCutscene())) {
		case CUTSCENE_SECOND:
			fioCut::startTextSliding();
			break;
		case CUTSCENE_SECOND * 28:
			isBaronOfHellRendered = false;
			initializeBaronOfHellAnimationChain();
			jjSample(TILE * 126.5, TILE * 226, SOUND::BILSBOSS_ZIP);
			break;
		case CUTSCENE_SECOND * 28 + 20:
			fio::increaseCutscenesWatchedIfFastForwardWasNotUsed(fioCut::wasFastForwardUsed);
			wasCutsceneWatchedTillTheEndBoss = true;
			endCutsceneBoss();
			break;
	}
}

void processCutsceneInterlude(jjPLAYER@ play) {
	switch(uint(fioCut::getElapsedCutscene())) {
		case CUTSCENE_SECOND:
			fioCut::startTextSliding();
			break;
		case CUTSCENE_SECOND * 2:
			lift.obj.state = STATE::FADEIN;
			break;
		case CUTSCENE_SECOND * 6:
			staticPlayerRenderState = STATE_STATIC_PLAYER_RENDER_FOCUSED;
			break;
		case CUTSCENE_SECOND * 18:
			staticPlayerRenderState = STATE_STATIC_PLAYER_RENDER_FRIGHTENED;
			break;
		case CUTSCENE_SECOND * 24:
			lift.obj.state = STATE::FADEOUT;
			staticPlayerRenderState = STATE_STATIC_PLAYER_RENDER_FALLING;
			jjSamplePriority(SOUND::BILSBOSS_SCARY3);
			jjSamplePriority(SOUND::INTRO_BOEM1);
			break;
	}
}

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

void processSummonedEnemies() {
	uint j = 0;
	uint summonedEnemiesLength = summonedEnemies.length();
	for (uint i = 0; i < summonedEnemiesLength; i++) {
		if (summonedEnemies[i].isActive && summonedEnemies[i].state != STATE::KILL) {
			@summonedEnemies[j] = summonedEnemies[i];
			j++;
		}
	}
	summonedEnemies.removeRange(j, summonedEnemiesLength - j);
}

void processSummoningFires() {
	uint j = 0;
	uint summoningFiresLength = summoningFires.length();
	for (uint i = 0; i < summoningFiresLength; i++) {
		if (!summoningFires[i].process(play)) {
			@summoningFires[j] = summoningFires[i];
			j++;
		}
	}
	summoningFires.removeRange(j, summoningFiresLength - j);
}

void processSummoningRocks() {
	uint j = 0;
	uint summoningRocksLength = summoningRocks.length();
	for (uint i = 0; i < summoningRocksLength; i++) {
		if (!summoningRocks[i].process()) {
			@summoningRocks[j] = summoningRocks[i];
			j++;
		} else {
			summoningFires.insertLast(
					SummoningFire(
							summoningRocks[i].x - SUMMONING_ROCK_OFFSET,
							summoningRocks[i].y - SUMMONING_ROCK_OFFSET
					)
			);
			
			jjOBJ@ enemyObj = jjObjects[
					jjAddObject(
							SUMMONING_ENEMIES[jjRandom() % SUMMONING_ENEMIES.length()],
							float(summoningRocks[i].x),
							float(summoningRocks[i].y)
					)
			];
			
			summonedEnemies.insertLast(enemyObj);
			enemyObj.direction = play.xPos < enemyObj.xPos ? -1 : 1;
			
			// Dragons cannot walk :-)
			if (enemyObj.eventID != OBJECT::DRAGON) {
				enemyObj.state = STATE::WALK;
			}
		}
	}
	summoningRocks.removeRange(j, summoningRocksLength - j);
}

void summonEnemiesFromTheSky() {
	array<uint> usedYLocations = array<uint>(0);
	
	// Hacky hack :)
	for (uint i = 0; i < SUMMONING_RANGES_X.length() - 1; i++) {
		uint amount = jjDifficulty * 1 + 1;
		array<uint> usedXLocations = array<uint>(0);
		
		for (uint j = 0; j < amount; j++) {
			uint randomX = getRandomUnusedLocationFromRange(SUMMONING_RANGES_X[i + 1] - SUMMONING_RANGES_X[i], usedXLocations);
			uint randomY = getRandomUnusedLocationFromRange(SUMMONING_RANGE_MAX_Y - SUMMONING_RANGE_MIN_Y, usedYLocations);
			usedXLocations.insertLast(randomX);
			usedYLocations.insertLast(randomY);
			summoningRocks.insertLast(
					SummoningRock(TILE * (SUMMONING_RANGES_X[i] + randomX), TILE * (SUMMONING_RANGE_MIN_Y + randomY))
			);
		}
	}
}

void summonPickupsFromTheSky() {
	array<uint> usedYLocations = array<uint>(0);
	array<OBJECT::Object> usedPickupIds;
	array<SummoningPickup@> pickupsToBeSummoned;
	
	// Hacky hack :)
	for (uint i = 0; i < SUMMONING_RANGES_X.length() - 1; i++) {
		uint amount = 4 - jjDifficulty;
		array<uint> usedXLocations = array<uint>(0);
		
		for (uint j = 0; j < amount; j++) {
			uint randomX = getRandomUnusedLocationFromRange(SUMMONING_RANGES_X[i + 1] - SUMMONING_RANGES_X[i], usedXLocations);
			uint randomY = getRandomUnusedLocationFromRange(SUMMONING_RANGE_MAX_Y - SUMMONING_RANGE_MIN_Y, usedYLocations);
			usedXLocations.insertLast(randomX);
			usedYLocations.insertLast(randomY);
			OBJECT::Object pickupId = getRandomUnusedPickup(usedPickupIds);
			usedPickupIds.insertLast(pickupId);
			pickupsToBeSummoned.insertLast(SummoningPickup(
					TILE * (SUMMONING_RANGES_X[i] + randomX),
					TILE * (SUMMONING_RANGE_MIN_Y + randomY),
					getRandomUnusedPickup(usedPickupIds))
			);
		}
	}
	
	pickupsToBeSummoned[jjRandom() % pickupsToBeSummoned.length()].pickupId = OBJECT::CARROT;
	
	for (uint i = 0; i < pickupsToBeSummoned.length(); i++) {
		SummoningPickup@ pickup = pickupsToBeSummoned[i];
		jjOBJ@ obj = jjObjects[jjAddObject(pickup.pickupId, pickup.x, pickup.y)];
		obj.state = STATE::FLOATFALL; // MAKE SURE TO SET THE STATE TO FLOATFALL
	}
}