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

funcdef void EFFECT_FUNC();

// 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) {
			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, 25);
			jjDrawRotatedSprite(obj.xPos + 20, obj.yPos - 36, ANIM::WITCH, 1, 55, 512, 2, 2);
		}
	}
	
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (@player !is null && player.charCurr == CHAR::FROG) {
			jjCharacters[CHAR::FROG].canHurt = true;
			
			// If hit by a frog tongue
			if (player.getObjectHitForce(obj) == -1) {
				if (!hasRockReturnTextBeenShown) {
					fioDraw::doShowText(9);
					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;
			}
			
			jjCharacters[CHAR::FROG].canHurt = false;
		}
		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;
	}
}

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 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 (y < SUMMONING_FLOOR_FIRST_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);
	}
}

class SwitchStone {
	int x;
	int y;
	bool isOn = false;
	EFFECT_FUNC@ effectFunc;
	SwitchStone(int x, int y, EFFECT_FUNC@ effectFunc) {
		this.x = x;
		this.y = y;
		@this.effectFunc = effectFunc;
	}
	void draw(jjCANVAS@ canvas) {
		canvas.drawTile(x, y + STONE_OFFSET_Y, isOn ? 65 : 66);
		canvas.drawTile(x, y - 32 + STONE_OFFSET_Y, isOn ? 55 : 56);
	}
	void switchOnIfPlayerInRangeAndInterludeWatched(jjPLAYER@ play) {
		// Offset player location by 16 pixels
		if (wasInterludeWatched && !isOn && abs(play.xPos - float(x + 16)) < 24 && abs(play.yPos - float(y)) < 32) {
			isOn = true;
			jjSamplePriority(SOUND::COMMON_BASE1);
			effectFunc();
		}
	}
}

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_X = TILE * 63;
const float INTERLUDE_Y = TILE * 85;
const float PLATFORM_OFFSET_X = -7;
const float PLATFORM_OFFSET_Y = 17;
const float RABBIT_FRIGHTENED_DURATION = CUTSCENE_SECOND * 20;
const float RABBIT_STILL_DURATION = CUTSCENE_SECOND * 7 - 15;

const int FLOOR_MOTION_INTERVAL = 5;
const int FLOOR_MOTION_LENGTH = 15;
const int FLOOR_MOTION_START_X = 63;
const int STONE_OFFSET_Y = 8;
const int SUMMONING_FIRE_DURATION = 140;
const int SUMMONING_FLOOR_FIRST_Y = TILE * 85;
const int SUMMONING_ROCK_SPEED_Y = 8;
const int SUMMONING_ROCK_OFFSET = 16;

const uint SUMMONING_RANGE_MAX_Y = 68;
const uint SUMMONING_RANGE_MIN_Y = 52;

const string NEXT_LEVEL_FILENAME = "Fio5_y.j2l";

const array<ArmoryItem@> ARMORY_ITEMS = {
	ArmoryItem(0, "||||Bouncer Power up@+ 20 ammo", 15, 52, ANIM::PICKUPS, 61, 0, @fio::sellArmoryItemBouncerPU),
	ArmoryItem(1, "||||Toaster Power up@+20 ammo", 15, 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", 10, 48, ANIM::PICKUPS, 62, 0, @fio::sellArmoryItemFreezerPU),
	ArmoryItem(4, "||||RF Power up@+20 ammo", 25, 48, ANIM::PICKUPS, 64, 0, @fio::sellArmoryItemRFPU),
	ArmoryItem(5, "||||+15 Seeker ammo", 20, 48, ANIM::AMMO, 37, 1, @fio::sellArmoryItemSeekerAmmo),
	ArmoryItem(6, "", 15, 44, ANIM::PICKUPS, 72, 5, @fio::sellArmoryItemInvincibility, @fio::canBuyInvincibility), // Text updated later when currentGameSession has been loaded
	ArmoryItem(7, "", 10, 56, ANIM::PICKUPS, 21, 0, @fio::sellArmoryItemPocketCarrot, @fio::canBuyPocketCarrot) // Text updated later when currentGameSession has been loaded
};

const array<Checkpoint@> FIO5_B_CHECKPOINTS = {
	Checkpoint(0, INTERLUDE_X, INTERLUDE_Y),
	Checkpoint(1, TILE * 63, TILE * 91),
	Checkpoint(2, TILE * 63, TILE * 49),
	Checkpoint(3, TILE * 21, TILE * 17)
};

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

const array<SwitchStone@> SWITCH_STONES = {
	SwitchStone(TILE * 51, TILE * 85, @summonEnemiesFromTheSky),
	SwitchStone(TILE * 57, TILE * 85, @summonEnemiesFromTheSky),
	SwitchStone(TILE * 69, TILE * 85, @summonEnemiesFromTheSky),
	SwitchStone(TILE * 75, TILE * 85, @summonEnemiesFromTheSky),
	SwitchStone(TILE * 49, TILE * 91, @summonHellKnightsPart),
	SwitchStone(TILE * 77, TILE * 91, @summonHellKnightsPart)
};

bool areHellKnightsActive = false;
bool areHellKnightsBeingSummoned = false;
bool areHellKnightsDefeated = false;
bool hasHellKnightsTextBeenShown = false;
bool hasPlatformTextBeenShown = false;
bool hasRockFreezeTextBeenShown = false;
bool hasRockReturnTextBeenShown = false;
bool hasSwitchStoneTextBeenDisplayed = false;
bool isFloorMotionActive = false;
bool isPlayerWaitingToBeWarpedAway = false;
bool isUpperSummoningFloorDefeated = false;
bool wasInterludeWatched = false;

int floorMotionOffsetX = 1;
int hellKnightSummoningElapsed = 0;
int playerWarpDelayElapsed = 0;

uint8 activeCutscene = uint8(CUTSCENE_NONE);

ANIM::Set playerAnimSet;

CharacterWithMindStone@ character;
HellKnight@ hellKnight1;
HellKnight@ hellKnight2;

array<uint> SUMMONING_RANGES_X = { 50, 57, 63, 69, 76 };

array<string> cutsceneTextsInterlude = {
	"||Greetings wanderer! Searching for someone? You'll find him from my lair! But before that, I want to test you a little more, to be sure that you're really worth it.",
	"|WHAT? WHO IS SPEAKING? SHOW YOURSELF!",
	"|That was no voice of Nicholas for sure. I may be facing a greater evil who wants to stop me right here where I am.",
	"|What is this room full of stones that look like my mindstone anyway?"
};

array<string> texts = {
	"|I really wonder why the demons decided to move Nicholas away from his cell. Why this move out of sudden?",
	"|I would really like to know what the demons use to move prisoners, while I have to creep through canyons full of traps like these.",
	"|Of course it's a TRAP! Or even a game? I guess my only choice is to kill all of these minions.",
	"", // Unused after migration to cutscene texts
	"", // 4th // Unused after migration to cutscene texts
	"|What? Platforms outta nowhere? I can't get anywhere with those...",
	"|What's happening now? Oh...it's these ones again!",
	"||Let's see how far can you run from my minions under a mutation spell! Hah hah!",
	"||Well! I've never met someone like you! I hope you can still push me around a bit. Stick your tongue into my eyes and I will go back.",
	"||Oof! Now that was a WET tongue!", // 9th
	"|Phew, finally back to my own form. How many more games do I have to play to find Nicholas and get outta here?"
};

array<jjOBJ@> summonedEnemies;

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

bool areHellKnightsDead() {
	return @hellKnight1 !is null && hellKnight1.isDead
			&& @hellKnight2 !is null && hellKnight2.isDead;
}

bool areAllUpperFloorSwitchStonesTurnedOn() {
	for (uint i = 0; i < 4; i++) {
		if (!SWITCH_STONES[i].isOn) {
			return false;
		}
	}
	return true;
}

void controlSummoningFloors() {
	if (!jjTriggers[14]
			&& areAllUpperFloorSwitchStonesTurnedOn()
			&& summoningFires.length() == 0
			&& summoningRocks.length() == 0
			&& isFirstSummoningFloorClearOfSummonedEnemies()) {
		jjTileSet(4, 63, 86, 0); // Start with the middle tile
		isFloorMotionActive = true;
		isUpperSummoningFloorDefeated = true;
		if (!checkpoints[1].isReached()) {
			checkpoints[1].setReached();
		}
	}
}

void controlSwitchStones(jjPLAYER@ play) {
	for (uint i = 0; i < SWITCH_STONES.length(); i++) {
		SWITCH_STONES[i].switchOnIfPlayerInRangeAndInterludeWatched(play);
	}
}

void createHellKnights() {
	// Make sure the total health doesn't exceed 127 as that's the limitation with obj.energy here (which will break the boss)
	@hellKnight1 = HellKnight(jjObjects[jjAddObject(OBJECT::EVA, TILE * 58, TILE * 87)], jjDifficulty * 25 + 45);
	@hellKnight2 = HellKnight(jjObjects[jjAddObject(OBJECT::EVA, TILE * 69, TILE * 87)], jjDifficulty * 25 + 45);
}

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 drawSwitchStones(jjCANVAS@ canvas) {
	for (uint i = 0; i < SWITCH_STONES.length(); i++) {
		SWITCH_STONES[i].draw(canvas);
	}
}

void endCutsceneInterlude() {
	wasInterludeWatched = true;
	fioCut::endCutscene(play.xPos, play.yPos);
	play.lighting = LIGHTING_TWILIGHT;
	activeCutscene = CUTSCENE_NONE;
	fioUtils::releasePlayer();
}

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

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

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

void initializeHellKnights() {
	hellKnightSummoningElapsed = 0;
	createHellKnights();
	areHellKnightsBeingSummoned = true;
	isPlayerUnableToMove = true;
	play.cameraFreeze(TILE * 63.5, TILE * 90, true, false);
	if (!hasHellKnightsTextBeenShown) {
		fioDraw::doShowText(6);
		hasHellKnightsTextBeenShown = true;
	}
}

void initializeInterlude() {
	fioCut::initializeCutscene(@processTickEvents, cutsceneTextsInterlude);
	fioCut::setTickEventsProcessed(true);
	play.noFire = true;
	activeCutscene = CUTSCENE_INTERLUDE;
}

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,
				play.xPos, play.yPos,
				play.xPos, play.yPos,
				0, 1, 1, playerAnimSet, character.idleAnimation, character.idleFrame, character.idleFrame, 1),
		fioCut::Animation(RABBIT_FRIGHTENED_DURATION,
				play.xPos, play.yPos,
				play.xPos, play.yPos,
				0, 1, 1, playerAnimSet, getRabbitAnimForFrightenedState(), 0, 0, 1)
	};
	
	fioCut::createAnimationChain(animationsInterludeRabbit);
}

void initializePossessedRocks() {
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 81.5, TILE * 37.75)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 81.5, TILE * 55.75)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 99.5, TILE * 37.75)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 99.5, TILE * 55.75)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 129.5, TILE * 73.75)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 68.5, TILE * 19.75)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 111.5, TILE * 19.75)]);
	PossessedRock(jjObjects[jjAddObject(OBJECT::BIGROCK, TILE * 135.5, TILE * 19.75)]);
}

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.0,
		2.0,
		2.0,
		2.0,
		2.0,
		2.0,
		2.5,
		2.5
	};
	
	array<uint16> tileIds = {
		10, 13
	};
	
	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],
						16,
						platformSpeeds[i],
						tileIds
				)
		);
	}
}

bool isFirstSummoningFloorClearOfSummonedEnemies() {
	return summonedEnemies.length() == 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);
	}
	
	if (isPlayerInArmory) {
		fioDraw::drawArmoryInterface(canvas);
	}
	
	return activeCutscene != CUTSCENE_NONE;
}

void onDrawLayer4(jjPLAYER@ play, jjCANVAS@ canvas) {
	if (activeCutscene != CUTSCENE_NONE) {
		fioCut::renderAnimations(canvas);
	}
	fio::renderCommon(play, canvas);
	drawSwitchStones(canvas);
	drawSummoningFires(canvas);
	drawSummoningRocks(canvas);
	fioDraw::drawArmoryAtPos(canvas, TILE * 177.5, TILE * 19.75); // Offset with +0.5 xTiles and +0.75 yTiles
}

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(10);
}

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

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

void onFunction5() {
	play.lighting = LIGHTING_TWILIGHT;
	if (!checkpoints[0].isReached()) {
		checkpoints[0].setReached();
		initializeInterlude();
	}
}

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

// onFunction7() obsolete

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

void onFunction9() {
	if (!checkpoints[3].isReached()) {
		checkpoints[3].setReached();
	}
	play.revertMorph();
	play.warpToID(3);
}

// onFunction10() obsolete

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

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

// Required for each level
void onLevelBegin() {
	@character = fio::getCharacterWithMindStoneForPlayer(play);
	initializePlatforms();
	initiatePlatformMovement();
	initializePossessedRocks();
	
	if (jjDifficulty == 0) {
		jjTriggers[25] = true;
	}
}

// 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_B_CHECKPOINTS);
	fioDraw::initializeDrawing(texts, array<string>(0), true);
	playerAnimSet = fio::getAnimSetForPlayer(jjLocalPlayers[0]);
	
	// Re-assignment via static declaration
	armoryItems = ARMORY_ITEMS;
	
	// Global indice
	armoryItemIndexInvincibility = 6;
	armoryItemIndexPocketCarrot = 7;
	
	armoryItems[armoryItemIndexInvincibility].text = fio::getInvincibilityItemText();
	armoryItems[armoryItemIndexPocketCarrot].text = fio::getPocketCarrotItemText();
	
	jjAnimSets[ANIM::WITCH].load();
	
	jjGenerateSettableTileArea(4, 49, 86, 29, 1);
	
	if (jjDifficulty <= 0) {
		jjTriggers[13] = true;
	}
}

// Required for each level
void onLevelReload() {
	MLLE::ReapplyPalette();
	reloadGlobals();
	fioDraw::initializeDrawing(texts, array<string>(0), true);
	
	initializePlatforms();
	initializePossessedRocks();
	initiatePlatformMovement();
	
	fio::handleLevelReload();
	
	if (!isUpperSummoningFloorDefeated) {
		for (uint i = 0; i < 4; i++) {
			SWITCH_STONES[i].isOn = false;
		}
	}
	
	if (!areHellKnightsDefeated) {
		for (uint i = 4; i < 6; i++) {
			SWITCH_STONES[i].isOn = false;
		}
		jjTriggers[15] = false;
	}
	
	// Just to ensure that the arrays does not go into some broken state if the player dies during the summoning floor fight
	summonedEnemies = array<jjOBJ@>(0);
	summoningFires = array<SummoningFire@>(0);
	summoningRocks = array<SummoningRock@>(0);
}

// Required for each level
void onMain() {
	fio::controlPressedKeys();
	fioDraw::controlHud();
	
	if (!isUpperSummoningFloorDefeated) {
		controlSummoningFloors();
		processSummonedEnemies();
		processSummoningFires();
		processSummoningRocks();
	}
}

// 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);
		}
	}
	
	controlSwitchStones(play);
	
	// TODO: Check wtf is wrong with these...Sometimes randomly doesn't seem to work...Add some debug lines with timestamp true...
	// Voisit harkita esim. jonkin "secret" tilen laittamista areenan seinään, josta saisi tarvittaessa debug datat ulos kun logiikka kusee ja sama varuiksi yläkertaan...
	if (areHellKnightsActive && areHellKnightsDead()) {
		areHellKnightsActive = false;
		areHellKnightsDefeated = true;
		jjTriggers[15] = false;
		isPlayerWaitingToBeWarpedAway = true;
		playerWarpDelayElapsed = 0;
		fio::rewardPoints(getPointRewardByDifficulty());
		
		if (!checkpoints[2].isReached()) {
			checkpoints[2].setReached();
		}
	}
	
	if (areHellKnightsBeingSummoned) {
		if (hellKnightSummoningElapsed < 350) {
			hellKnightSummoningElapsed++;
		} else {
			fioUtils::releasePlayer();
			areHellKnightsBeingSummoned = false;
			areHellKnightsActive = true;
			hellKnight1.appearElapsed = 350;
			hellKnight2.appearElapsed = 350;
			play.cameraUnfreeze(false);
		}
	}
	
	if (isFloorMotionActive) {
		if (jjGameTicks % FLOOR_MOTION_INTERVAL == 0 && floorMotionOffsetX < FLOOR_MOTION_LENGTH) {
			jjTileSet(4, 63 + floorMotionOffsetX, 86, 0);
			jjTileSet(4, 63 - floorMotionOffsetX, 86, 0);
			floorMotionOffsetX++;
		} else if (floorMotionOffsetX >= FLOOR_MOTION_LENGTH) {
			isFloorMotionActive = false;
		}
	}
	
	if (isPlayerWaitingToBeWarpedAway) {
		if (playerWarpDelayElapsed < 105) {
			playerWarpDelayElapsed++;
		} else {
			isPlayerWaitingToBeWarpedAway = false;
			play.warpToTile(63, 49);
		}
	}
}

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

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

void processCutsceneInterlude(jjPLAYER@ play) {
	switch(uint(fioCut::getElapsedCutscene())) {
		case CUTSCENE_SECOND:
			isPlayerHiddenAndUnableToMove = true;
			initializeInterludeAnimationChain();
			fioCut::startTextSliding();
			break;
		case CUTSCENE_SECOND * 26 + 10:
			endCutsceneInterlude();
			fio::increaseCutscenesWatchedIfFastForwardWasNotUsed(fioCut::wasFastForwardUsed);
			break;
	}
}

void processTickEvents(jjPLAYER@ play) {
	switch (activeCutscene) {
		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 {
			if (!hasSwitchStoneTextBeenDisplayed) {
				fioDraw::doShowText(2);
				hasSwitchStoneTextBeenDisplayed = true;
			}
			
			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;
			enemyObj.points = 0;
			
			// 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 summonHellKnightsPart() {
	if (!jjTriggers[15]) {
		jjTriggers[15] = true;
		if (!hasPlatformTextBeenShown) {
			fioDraw::doShowText(5);
			hasPlatformTextBeenShown = true;
		}
	} else {
		initializeHellKnights();
	}
}