Downloads containing Fio_entities.asc

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

File preview

#include "Fio_drawing.asc"

funcdef bool CAN_BUY_ARMORY_ITEM_FUNC();
funcdef void SELL_ARMORY_ITEM_FUNC();

class ArmoryItem {

	string text;
	float currentScale;
	float finalScale;
	float currentY;
	float finalY;
	int animSetId;
	int cost;
	int motionElapsed;
	int textOffsetY;
	uint8 anim;
	uint8 frame;
	
	CAN_BUY_ARMORY_ITEM_FUNC@ canBuyFunc;
	SELL_ARMORY_ITEM_FUNC@ sellFunc;
	
	ArmoryItem(uint index, string text, int cost, int textOffsetY, int animSetId, uint8 anim, uint8 frame,
			SELL_ARMORY_ITEM_FUNC@ sellFunc,
			CAN_BUY_ARMORY_ITEM_FUNC@ canBuyFunc = null) {
		this.text = text;
		this.cost = cost;
		this.textOffsetY = textOffsetY;
		this.animSetId = animSetId;
		this.anim = anim;
		this.frame = frame;
		@this.canBuyFunc = canBuyFunc;
		@this.sellFunc = sellFunc;
		motionElapsed = ARMORY_ITEM_MOTION_DURATION;
		const float scale = getScaleByIndex(index);
		currentScale = scale;
		finalScale = scale;
		const float y = getYByIndex(index);
		currentY = y;
		finalY = y;
	}
	
	bool canBuy() {
		if (@canBuyFunc !is null) {
			return canBuyFunc();
		}
		return true;
	}
	
	void draw(jjCANVAS@ canvas, uint index, uint armoryItemsLength) {
		const float scaleDistance = finalScale - currentScale;
		const float scale = currentScale + float(motionElapsed) / float(ARMORY_ITEM_MOTION_DURATION) * scaleDistance;
		const float yDistance = finalY - currentY;
		const int y = int(currentY + float(motionElapsed) / float(ARMORY_ITEM_MOTION_DURATION) * yDistance);
		const string costColorPipes = play.coins >= cost ? "|" : "||";
		const string costString = "Cost: " + costColorPipes + cost + "x";
		canvas.drawResizedSprite(jjSubscreenWidth / 2, y, animSetId, anim, frame, scale, scale);
		if (isSelected(index)) {
			canvas.drawString(jjSubscreenWidth / 2, y + textOffsetY, text, STRING::SMALL, centeredText);
			canvas.drawString(jjSubscreenWidth / 2, y + textOffsetY + 48, costString, STRING::SMALL, centeredText);
			canvas.drawSprite(jjSubscreenWidth / 2 + 16 + jjGetStringWidth(costString, STRING::SMALL, centeredText)  / 2, y + textOffsetY + 48,
					ANIM::PICKUPS, 84, 0 ,0);
		}
		if (motionElapsed < ARMORY_ITEM_MOTION_DURATION) {
			motionElapsed++;
		} else {
			currentY = finalY;
			currentScale = finalScale;
		}
	}
	
	void move(uint index) {
		finalY = getYByIndex(index);
		finalScale = getScaleByIndex(index);
		motionElapsed = 0;
	}
	
	void sell() {
		if (@this.sellFunc !is null) {
			this.sellFunc();
		}
	}
	
	private float getScaleByIndex(uint index) {
		return index == selectedArmoryItem ? 2.f : 1.f;
	}
	
	private float getYByIndex(uint index) {
		const float itemDistance = selectedArmoryItem < index ? 144.f : 72.f;
		return float(jjSubscreenHeight / 4 + 32) + float((int(index) - selectedArmoryItem)) * itemDistance;
	}
	
	private bool isSelected(uint index) {
		return index == selectedArmoryItem;
	}
}

class BossFromHell : jjBEHAVIORINTERFACE {

	float healthY = 40.f;

	int energy;
	int maxEnergy;
	
	jjOBJ@ obj;
	
	private jjBEHAVIOR nativeBehavior;
	
	BossFromHell(jjOBJ@ obj, int maxEnergy) {
		@this.obj = obj;
		nativeBehavior = obj.behavior;
		obj.behavior = this;
		obj.scriptedCollisions = true;
		obj.bulletHandling = HANDLING::DETECTBULLET;
		obj.playerHandling = HANDLING::SPECIAL;
		this.maxEnergy = maxEnergy;
		energy = maxEnergy;
	}
	
	void onBehave(jjOBJ@ obj) override {
		if (obj.state == STATE::KILL) {
			currentGameSession.enemiesSlain++;
			removeFromEventMap(obj);
		}
		obj.behave(nativeBehavior);
	}
	
	void onDraw(jjOBJ@ obj) {
		if (isCustomBossActivated) {
			drawHealth(obj);
		}
	}
	
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) {
		if (@bullet != null) {
			energy -= force;
			obj.justHit = 5;
			bullet.delete();
		}
		if (@play != null && force < 1) {
			play.hurt(1);
		}
		return true;
	}
	
	private void drawHealth(jjOBJ@ obj) {
		float initialX = obj.xPos - BOSS_HEALTH_BAR_OFFSET_X;
		
		for (int i = 0; i < BOSS_HEALTH_BAR_LENGTH; ++i) {
			jjDrawSprite(initialX + i, obj.yPos - healthY, ANIM::PICKUPS, 41, 0, 0, SPRITE::SINGLECOLOR, 41);
		}
		
		float currentHealthLength = float(energy) / float(maxEnergy) * float(BOSS_HEALTH_BAR_LENGTH);
		
		for (float i = 0.f; i < currentHealthLength; ++i) {
			jjDrawSprite(initialX + i, obj.yPos - healthY, ANIM::PICKUPS, 41, 0, 0, SPRITE::SINGLECOLOR, 24);
		}
	}
}

class CharacterWithMindStone {
	int mindStoneX;
	int mindStoneY;
	ANIM::Set animSet;
	uint8 idleAnimation;
	uint8 idleFrame;
	uint8 withMindStoneAnimation;
	uint8 withMindStoneFrame;
	uint8 digAnimation;
	uint8 digFrameStart;
	uint8 digFrameEnd;
	CharacterWithMindStone(
			int mindStoneX,
			int mindStoneY,
			ANIM::Set animSet,
			uint8 idleAnimation,
			uint8 idleFrame,
			uint8 withMindStoneAnimation,
			uint8 withMindStoneFrame,
			uint8 digAnimation,
			uint8 digFrameStart,
			uint8 digFrameEnd) {
		this.mindStoneX = mindStoneX;
		this.mindStoneY = mindStoneY;
		this.animSet = animSet;
		this.idleAnimation = idleAnimation;
		this.idleFrame = idleFrame;
		this.withMindStoneAnimation = withMindStoneAnimation;
		this.withMindStoneFrame = withMindStoneFrame;
		this.digAnimation = digAnimation;
		this.digFrameStart = digFrameStart;
		this.digFrameEnd = digFrameEnd;
	}
}

/* 
	Properties yOrg and xOrg of jjPLAYER must be set in onLevelReload, as they are effective only during the
		level reload lifecycle.
*/
class Checkpoint {

	float xOrg;
	float yOrg;
	bool reached;
	int id;
	
	Checkpoint(int id, float xOrg, float yOrg) {
		this.id = id;
		this.xOrg = xOrg;
		this.yOrg = yOrg;
		reached = false;
	}
	
	bool isReached() {
		return reached;
	}
	
	void setReached() {
		reached = true;
		jjAlert("Checkpoint reached!", false, STRING::LARGE);
	}
}

class DestructSceneryWithHealthBar : jjBEHAVIORINTERFACE {

	DestructSceneryWithHealthBar() {}
	
	void onBehave(jjOBJ@ obj) override {	
		obj.behave(BEHAVIOR::DESTRUCTSCENERY);
	}
	
	void onDraw(jjOBJ@ obj) {
		// Use maxEnergy > 1 hack to identify the ones that should have health bar displayed
		if (obj.isActive && obj.var[1] > 1 && obj.energy > 0) {
			drawHealth(obj);
		}
	}

	private void drawHealth(jjOBJ@ obj) {
		// Recycle boss health bar code cause imma be lazy
		float healthBarInitialX = obj.xPos - DESTRUCT_SCENERY_HEALTH_BAR_OFFSET_X;
		float maxEnergy = float(obj.var[1]); // The number of frames in the animated tile
			
		for (int i = 0; i < DESTRUCT_SCENERY_HEALTH_BAR_LENGTH; ++i) {
			jjDrawSprite(healthBarInitialX + i, obj.yPos - DESTRUCT_SCENERY_HEALTH_BAR_OFFSET_Y, ANIM::PICKUPS, 41, 0, 0, SPRITE::SINGLECOLOR, 41, 2);
		}
		
		float currentHealthLength = float(obj.energy) / float(maxEnergy) * float(DESTRUCT_SCENERY_HEALTH_BAR_LENGTH);
		
		for (float i = 0.f; i < currentHealthLength; ++i) {
			jjDrawSprite(healthBarInitialX + i, obj.yPos - DESTRUCT_SCENERY_HEALTH_BAR_OFFSET_Y, ANIM::PICKUPS, 41, 0, 0, SPRITE::SINGLECOLOR, 24, 2);
		}
	}
}

class GameSession {
	
	bool hasBonusLevelBeenPlayed;
	bool hasPlayerDiedPreviously;
	bool isFinished; // Maybe not necessarily needed after all, since the status can be checked with finishedTime as well, but w/e at this point...
	
	int difficulty;
	int food; // Original food value stored while "identifying" the game session more reliably using the jjPLAYER::food property with a random value
	int id;
	int score;
	
	int64 finishedTime;
	int64 startedTime;
	int64 totalTime;
	
	// No need to store foodCollected, since player.food already stores that cumulatively over a playthrough
	uint cutscenesWatched;
	uint deathCount;
	uint enemiesSlain;
	uint purpleGemsCollected;
	
	// Store the main state of these here in the GameSession, but store the preserved amount for boss fights in the Player-state
	uint8 invincibilities;
	uint8 pocketCarrots;
	
	CHAR::Char charOrig;
	
	GameSession() {
		hasBonusLevelBeenPlayed = false;
		hasPlayerDiedPreviously = false;
		isFinished = false;
		difficulty = 1; // Default medium
		food = 0;
		id = 0;
		score = 0;
		finishedTime = 0;
		startedTime = 0;
		totalTime = 0;
		cutscenesWatched = 0;
		deathCount = 0;
		enemiesSlain = 0;
		purpleGemsCollected = 0;
		invincibilities = 0;
		pocketCarrots = 0;
		charOrig = CHAR::JAZZ;
	}
}

class Gun : jjBEHAVIORINTERFACE {

	void onBehave(jjOBJ@ obj) override {
		obj.behave(BEHAVIOR::EVA);
	}
	
	void onDraw(jjOBJ@ obj) {
		switch (play.charOrig)
		{
			case CHAR::LORI:
				obj.determineCurAnim(ANIM::LORI2, 1);
				obj.frameID = 0;
				break;
			case CHAR::SPAZ:
				obj.determineCurAnim(ANIM::AMMO, 19);
				obj.frameID = 0;
				break;
			case CHAR::JAZZ:
				default:
				obj.determineCurAnim(ANIM::AMMO, 18);
				obj.frameID = 0;
				break;
		}
	}
}

class HellKnight : jjBEHAVIORINTERFACE {

	jjOBJ@ obj;
	array<RingBall@> ringBalls;
	// Init attributes
	bool initSecondaryAttack = true;
	bool isDead = false;
	bool isAttackInitiated = false;
	float healthY = 40.f;
	float ringDistance = 64.f;
	float targetX = 0.f; //play.xPos
	float targetY = 0.f; //play.yPos
	float xPos;
	float yPos;
	int appearElapsed = 0;
	int attackCountdown = 0;
	int elapsedAttackInitiate = 0;
	int8 maxEnergy;
	SPRITE::Mode highlightType = SPRITE::SINGLEHUE;
	uint attackChargeElapsed = 0;
	uint attackFlyElapsed = 0;
	uint attackReturnElapsed = 0;
	uint randomInterval = 1023;
	uint ringElapsed = 0;
	uint shootElapsed = 0;
	uint8 attackState = STATE_ATTACK_CHARGE;
	uint8 hellKnightState = STATE_HELL_KNIGHT_IDLE;
	uint8 nextState = STATE_HELL_KNIGHT_ATTACK_EXPAND_RING;
	
	HellKnight() {}
	
	HellKnight(jjOBJ@ obj, int8 maxEnergy) {
		@this.obj = obj;
		this.xPos = obj.xPos;
		this.yPos = obj.yPos;
		this.maxEnergy = maxEnergy;
		
		obj.behavior = this;
		obj.bulletHandling = HANDLING::HURTBYBULLET;
		obj.playerHandling = HANDLING::SPECIAL;
		obj.scriptedCollisions = true;
		obj.energy = maxEnergy;
		
		// Initialize custom properties
		isDead = false;
		initSecondaryAttack = true;
		isAttackInitiated = false;
		attackChargeElapsed = 0.f;
		attackFlyElapsed = 0.f;
		attackReturnElapsed = 0.f;
		healthY = 40.f;
		ringElapsed = 0.f;
		ringDistance = 64.f;
		shootElapsed = 0.f;
		targetX = 0.f; //play.xPos
		targetY = 0.f; //play.yPos
		appearElapsed = 0;
		attackCountdown = 0;
		elapsedAttackInitiate = 0;
		randomInterval = 1023;
		highlightType = SPRITE::SINGLEHUE;
		attackState = STATE_ATTACK_CHARGE;
		hellKnightState = STATE_HELL_KNIGHT_APPEAR;
			
		for (int i = 0; i < 12; ++i) {
			int objId = jjAddObject(OBJECT::APPLE, xPos + ringDistance * (jjSin((i + 1) * 85)),
					yPos + ringDistance * (jjCos((i + 1) * 85)));
			ringBalls.insertLast(RingBall(jjObjects[objId]));
		}
	}
	
	void onBehave(jjOBJ@ obj) override {
		faceTowardsPlayer(obj);
		handleAttackCountdown();
		handleHellKnightState(obj);
		if (ringElapsed == randomInterval) {
			shootElapsed = 0;
			hellKnightState = nextState;
		}
		if (isAttackInitiated) {
			handleAttackInitiation();
		}
		switch (obj.state) {
			case STATE::KILL:
			{
				for (uint i = 0; i < ringBalls.length(); ++i) {
					ringBalls[i].ballState = STATE_BALL_FALL;
					ringBalls[i].obj.state = STATE::FLOATFALL;
				}
				ringBalls.removeRange(0, 12);
				jjAddParticlePixelExplosion(xPos, yPos, obj.curFrame, obj.direction, 1);
				obj.delete();
				isDead = true;
			}
				break;
			case STATE::IDLE:
			{
				obj.determineCurFrame();
			}
				break;
		}
	}
	
	void onDraw(jjOBJ@ obj) {
		SPRITE::Mode spriteMode = obj.justHit > 0 ? SPRITE::SINGLECOLOR : SPRITE::TINTED;
		uint8 spriteModeParam = obj.justHit > 0 ? 15 : 24;
		
		switch (hellKnightState) {
			case STATE_HELL_KNIGHT_APPEAR:
			{
				obj.determineCurAnim(ANIM::DEMON, 3);
				obj.frameID = 3;
				obj.determineCurFrame();
				if (appearElapsed < 192) {
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction,
					SPRITE::BLEND_DISSOLVE, appearElapsed);
				} else {
					jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction,
					spriteMode, spriteModeParam);
				}
			}
				break;
			case STATE_HELL_KNIGHT_ATTACK_EXPAND_RING:
			{
				obj.determineCurAnim(ANIM::DEMON, 3);
				obj.frameID = 2;
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame,obj.direction, spriteMode, spriteModeParam);
			}
				break;
			case STATE_HELL_KNIGHT_ATTACK_FOCUS_RING:
			{
				obj.determineCurAnim(ANIM::DEMON, 1);
				obj.frameID = 0;
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame,obj.direction, spriteMode, spriteModeParam);
			}
				break;
			case STATE_HELL_KNIGHT_IDLE:
			default:
			{
				obj.determineCurAnim(ANIM::DEMON, 3);
				obj.frameID = 3;
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame,obj.direction, spriteMode, spriteModeParam);
			}
				break;
		}
		drawHealth(obj);
	}
	
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) {
		if (@bullet != null) {
			bullet.delete();
		}
		if (@play != null && force < 1) {
			if (play.xPos > obj.xPos) {
				play.xPos += 24;
				play.direction = -1;
			} else {
				play.xPos -= 24;
				play.direction = 1;
			}
			if (play.yPos > obj.yPos) {
				play.yPos += 24;
			} else {
				play.yPos -= 24;
			}
			play.hurt(1, true);
		}
		return true;
	}
	
	private void doRandomInterval(jjOBJ@ obj) {
		elapsedAttackInitiate = 0; // just in case
		randomInterval = jjRandom() % 1024;
		if (ringElapsed > randomInterval) {
			attackCountdown = 1024 - ringElapsed + randomInterval;
		} else {
			attackCountdown = randomInterval - ringElapsed;
		}
		
		// Hax on already glitchy code, hoping to make it a little less glitchy, although not more readable...
		if (attackCountdown < 256) {
			if (ringElapsed + 256 > 1023) {
				randomInterval = 256 - (1024 - ringElapsed);
			} else {
				randomInterval = ringElapsed + 256;
			}
			attackCountdown = 256;
		} else if (attackCountdown > 511) {
			if (ringElapsed + 511 > 1023) {
				randomInterval = 511 - (1024 - ringElapsed);
			} else {
				randomInterval = ringElapsed + 511;
			}
			attackCountdown = 511;
		}
	}
	
	private void doSecondaryAttack() {
		switch (attackState) {
			case STATE_ATTACK_CHARGE:
			{
				for (uint i = 0; i < ringBalls.length(); ++i) {
					ringBalls[i].obj.xPos = ringBalls[i].obj.xPos - (ringBalls[i].obj.xPos - targetX) * jjSin(attackChargeElapsed / 2);
					ringBalls[i].obj.yPos = ringBalls[i].obj.yPos - (ringBalls[i].obj.yPos - targetY) * jjSin(attackChargeElapsed / 2);
				}
				
				if (attackChargeElapsed < 170) {
					attackChargeElapsed++;
				} else {
					attackState = STATE_ATTACK_FLY;
				}
			}
			break;
			case STATE_ATTACK_FLY:
			{
				for (uint i = 0; i < ringBalls.length(); ++i) {
					ringBalls[i].obj.xPos = targetX + (ringDistance * jjSin(attackFlyElapsed)) * (jjSin((i + 1) * 85 + ringElapsed));
					ringBalls[i].obj.yPos = targetY + (ringDistance * jjSin(attackFlyElapsed)) * (jjCos((i + 1) * 85 + ringElapsed));
				}
				
				if (attackFlyElapsed < 170) {
					attackFlyElapsed++;
				} else {
					attackState = STATE_ATTACK_RETURN;
				}
				if (ringElapsed >= 1023) ringElapsed = 0;
			}
			break;
			case STATE_ATTACK_RETURN:
			{
				for (uint i = 0; i < ringBalls.length(); ++i) {
					ringBalls[i].obj.xPos = ringBalls[i].obj.xPos - (ringBalls[i].obj.xPos - (xPos + ringDistance * (jjSin((i + 1) * 85 + ringElapsed)))) * jjSin(attackReturnElapsed / 2);
					ringBalls[i].obj.yPos = ringBalls[i].obj.yPos - (ringBalls[i].obj.yPos - (yPos + ringDistance * (jjCos((i + 1) * 85 + ringElapsed)))) * jjSin(attackReturnElapsed / 2);
				}
				
				if (attackReturnElapsed < 170) {
					attackReturnElapsed++;
				}
			}
			break;
		}
	}
	
	private void drawHealth(jjOBJ@ obj) {
		float initialX = obj.xPos - BOSS_HEALTH_BAR_OFFSET_X;
		
		for (int i = 0; i < BOSS_HEALTH_BAR_LENGTH; ++i) {
			jjDrawSprite(initialX + i, obj.yPos - healthY, ANIM::PICKUPS, 41, 0, 0, SPRITE::SINGLECOLOR, 41);
		}
		
		float currentHealthLength = float(obj.energy) / float(maxEnergy) * float(BOSS_HEALTH_BAR_LENGTH);
		
		for (float i = 0.f; i < currentHealthLength; ++i) {
			jjDrawSprite(initialX + i, obj.yPos - healthY, ANIM::PICKUPS, 41, 0, 0, SPRITE::SINGLECOLOR, 24);
		}
	}
	
	private void faceTowardsPlayer(jjOBJ@ obj) {
		if (play.xPos < obj.xPos) obj.direction = -1;
		else obj.direction = 0;
	}
	
	private void handleAttackCountdown() {
		if (attackCountdown > 0) attackCountdown--;
		if (attackCountdown == 70) {
			elapsedAttackInitiate = 0;
			nextState = jjRandom() % 2 == 0 ? STATE_HELL_KNIGHT_ATTACK_EXPAND_RING
					: STATE_HELL_KNIGHT_ATTACK_FOCUS_RING;
			highlightType = nextState == STATE_HELL_KNIGHT_ATTACK_EXPAND_RING
					? SPRITE::SINGLEHUE : SPRITE::SINGLECOLOR;
			
			for (uint i = 0; i < ringBalls.length(); ++i) {
				ringBalls[i].highlightType = highlightType;
			}
			
			isAttackInitiated = true;
		}
	}
	
	private void handleAttackInitiation() {
		if (elapsedAttackInitiate < 70) {
			elapsedAttackInitiate++;
			if (nextState == STATE_HELL_KNIGHT_ATTACK_EXPAND_RING) {
				for (uint i = 0; i < ringBalls.length(); ++i) {
					ringBalls[i].elapsedAttackInitiate = elapsedAttackInitiate;
				}
			}
		} else {
			isAttackInitiated = false;
			highlightType = SPRITE::SINGLEHUE;
			for (uint i = 0; i < ringBalls.length(); ++i) {
				ringBalls[i].highlightType = highlightType;
				ringBalls[i].elapsedAttackInitiate = 24;
			}
		}
	}
	
	private void handleHellKnightState(jjOBJ@ obj) {
		switch (hellKnightState) {
			case STATE_HELL_KNIGHT_APPEAR:
			{
				obj.light = 10;
				obj.lightType = LIGHT::NORMAL;
				obj.state = STATE::HIDE;
				
				if (appearElapsed < 350) {
					++appearElapsed;
				} else {
					obj.state = STATE::IDLE;
					hellKnightState = STATE_HELL_KNIGHT_IDLE;
					for (uint i = 0; i < ringBalls.length(); ++i) {
						ringBalls[i].dieCounter = 255;
					}
					doRandomInterval(obj);
				}
			}
			break;
			case STATE_HELL_KNIGHT_ATTACK_EXPAND_RING:
			{
				obj.behave(BEHAVIOR::EVA);
				obj.light = 10;
				obj.lightType = LIGHT::NORMAL;
				++ringElapsed;
				ringDistance += 2.f * jjCos(shootElapsed);
				
				if (shootElapsed < 512) {
					++shootElapsed;
				} else {
					doRandomInterval(obj);
					hellKnightState = STATE_HELL_KNIGHT_IDLE;
				}
				
				rotateRing(obj);
				if (obj.energy <= 0) hellKnightState = STATE_HELL_KNIGHT_DIE;
			}
			break;
			case STATE_HELL_KNIGHT_ATTACK_FOCUS_RING:
			{
				if (initSecondaryAttack) {
					targetX = play.xPos;
					targetY = play.yPos;
					attackChargeElapsed = 0;
					attackFlyElapsed = 0.f;
					attackReturnElapsed = 0.f;
					attackState = STATE_ATTACK_CHARGE;
					initSecondaryAttack = false;
				}
				obj.behave(BEHAVIOR::EVA);
				obj.light = 10;
				obj.lightType = LIGHT::NORMAL;
				++ringElapsed;
				
				// Hacky spaghetti...
				if (ringElapsed >= 1023) {
					ringElapsed = 0;
				}
				
				if (shootElapsed < 512) {
					++shootElapsed;
				} else {
					doRandomInterval(obj);
					initSecondaryAttack = true;
					hellKnightState = STATE_HELL_KNIGHT_IDLE;
				}
				
				doSecondaryAttack();
				if (obj.energy <= 0) hellKnightState = STATE_HELL_KNIGHT_DIE;
			}
			break;
			default:
			{
				obj.behave(BEHAVIOR::EVA);
				obj.light = 10;
				obj.lightType = LIGHT::NORMAL;
				++ringElapsed;
				
				rotateRing(obj);
				if (obj.energy <= 0) {
					hellKnightState = STATE_HELL_KNIGHT_DIE;
				}
			}
			break;
		}
	}
	
	private void rotateRing(jjOBJ@ obj) {
		for (uint i = 0; i < ringBalls.length(); ++i) {
			ringBalls[i].obj.xPos = xPos + ringDistance * (jjSin((i + 1) * 85 + ringElapsed));
			ringBalls[i].obj.yPos = yPos + ringDistance * (jjCos((i + 1) * 85 + ringElapsed));
		}
		
		if (ringElapsed >= 1023) {
			ringElapsed = 0;
		}
	}
}

class Key {

	bool isPressed;
	
	Key() {
		isPressed = false;
	}
}

class Node {
	float x;
	float y;
	Node(float x, float y) {
		this.x = x;
		this.y = y;
	}
}

// It seems to be tricky to make this a freezable platform, since that probably requires custom pixelmap, etc. So just keep
// it as it is for now...
class Platform : jjBEHAVIORINTERFACE {

	// Stored obj handle for state management outside the class
	jjOBJ@ obj;
	array<uint16> tileIds;
	array<Node@>@ nodes;
	float renderOffsetY;
	float speed;
	int8 layerZ;
	uint nextNodeIndex = 1;
	
	Platform(jjOBJ@ obj, array<Node@>@ nodes, float renderOffsetY, float speed, array<uint16> tileIds, int8 layerZ = 4) {
		@this.obj = obj;
		// MAKE SURE to use array handles (references) instead of passing whole array copies, since AS will result in passing null
		// for that array parameter in some cases, especially if the arrays have been initialized as nested or something...
		@this.nodes = nodes;
		this.renderOffsetY = renderOffsetY;
		this.speed = speed;
		this.tileIds = tileIds;
		this.layerZ = layerZ;
		obj.behavior = this;
		obj.deactivates = false;
		obj.isFreezable = true;
		obj.state = STATE::WAIT;
	}
	
	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:
				{
					if (nodes.length() >= 2) {
						int currentNodeIndex = nextNodeIndex >= 1 ? nextNodeIndex - 1 : nodes.length() - 1;
						float xDistance = nodes[nextNodeIndex].x - obj.xPos;
						float yDistance = nodes[nextNodeIndex].y - obj.yPos;
						
						if (floor(abs(xDistance)) == 0 && floor(abs(yDistance)) == 0) {
							if (nextNodeIndex >= nodes.length() - 1) {
								nextNodeIndex = 0;
							} else {
								nextNodeIndex++;
							}
						} else {
							if (floor(xDistance) > 0) {
								if (xDistance - speed < 0) {
									obj.xPos = nodes[nextNodeIndex].x;
								} else {
									obj.xPos += speed;
								}
							} else if (ceil(xDistance) < 0) {
								if (xDistance + speed > 0) {
									obj.xPos = nodes[nextNodeIndex].x;
								} else {
									obj.xPos -= speed;
								}
							}
							if (floor(yDistance) > 0) {
								if (yDistance - speed < 0) {
									obj.yPos = nodes[nextNodeIndex].y;
								} else {
									obj.yPos += speed;
								}
							} else if (ceil(yDistance) < 0) {
								if (yDistance + speed > 0) {
									obj.yPos = nodes[nextNodeIndex].y;
								} else {
									obj.yPos -= speed;
								}
							}
						}
					}
				}
				break;
			case STATE::DEACTIVATE:
				obj.deactivate();
				return;
		}
		const uint widthInTiles = tileIds.length() == 6 ? 3 : 2;
		obj.bePlatform(xOld, yOld, TILE * widthInTiles + 8, 16);
	}
	
	void onDraw(jjOBJ@ obj) {
		// Assuming that every platform is at least 2 tiles wide
		jjDrawTile(obj.xPos + 8, obj.yPos - TILE + renderOffsetY, tileIds[0], TILE::ALLQUADRANTS, layerZ);
		jjDrawTile(obj.xPos + 40, obj.yPos - TILE + renderOffsetY, tileIds[1], TILE::ALLQUADRANTS, layerZ);
		if (tileIds.length() == 4) { // If height is 2 tiles instead of 1 tile
			jjDrawTile(obj.xPos + 8, obj.yPos + renderOffsetY, tileIds[2], TILE::ALLQUADRANTS, layerZ);
			jjDrawTile(obj.xPos + 40, obj.yPos + renderOffsetY, tileIds[3], TILE::ALLQUADRANTS, layerZ);
		} else if (tileIds.length() == 6) { // If width is 3 tiles and height is 2
			jjDrawTile(obj.xPos + 72, obj.yPos - TILE + renderOffsetY, tileIds[2], TILE::ALLQUADRANTS, layerZ);
			jjDrawTile(obj.xPos + 8, obj.yPos + renderOffsetY, tileIds[3], TILE::ALLQUADRANTS, layerZ);
			jjDrawTile(obj.xPos + 40, obj.yPos + renderOffsetY, tileIds[4], TILE::ALLQUADRANTS, layerZ);
			jjDrawTile(obj.xPos + 72, obj.yPos + renderOffsetY, tileIds[5], TILE::ALLQUADRANTS, layerZ);
		}
	}
}

class Player {
	
	int coins;
	
	// The preserved amount
	uint8 invincibilities;
	uint8 pocketCarrots;
	
	array<int> ammo(10, 0);
	array<int> gems(5, 0);
	array<bool> powerups(10, false);
	
	Player(jjPLAYER@ play) {
		coins = play.coins;
		savePlayerGems(play);
		savePlayerEquipment(play);
	}
	
	/* Certain jjPLAYER properties can be loaded in onLevelReload, such as jjPLAYER::score as those
		properties do not get initialized afterwards.
		
		Assigning other jjPLAYER properties, like ammo, gems. etc need to be loaded after the level reload
		lifecycle, e.g. first call on onPlayer(). Therefore, call this method there in a block covered by
		a comparison of !playerPropertiesReinitialized.
	*/
	void loadPlayerProperties(jjPLAYER@ play) {
		play.coins = coins;
		loadPlayerGems(play);
		loadPlayerEquipment(play);
	}
	
	void savePlayerProperties(jjPLAYER@ play) {
		coins = play.coins;
		savePlayerGems(play);
		
		if (shouldStorePlayerEquipment) {
			savePlayerEquipment(play);
		}
	}
	
	private void loadPlayerEquipment(jjPLAYER@ play) {
		for (int i = 2; i <= 9; ++i) { // No Blaster PU in this episode anyway
			play.ammo[i] = ammo[i];
			play.powerup[i] = powerups[i];
		}
		
		if (@currentGameSession !is null) {
			currentGameSession.invincibilities = invincibilities;
			currentGameSession.pocketCarrots = pocketCarrots;
		}
	}
	
	private void loadPlayerGems(jjPLAYER@ play) {
		play.gems[GEM::RED] = gems[GEM::RED];
		play.gems[GEM::GREEN] = gems[GEM::GREEN];
		play.gems[GEM::BLUE] = gems[GEM::BLUE];
		play.gems[GEM::PURPLE] = gems[GEM::PURPLE];
	}
	
	private void savePlayerEquipment(jjPLAYER@ play) {
		for (int i = 2; i <= 9; ++i) { // No Blaster PU in this episode anyway
			ammo[i] = play.ammo[i];
			powerups[i] = play.powerup[i];
		}
		
		invincibilities = @currentGameSession !is null ? currentGameSession.invincibilities : 0;
		pocketCarrots = @currentGameSession !is null ? currentGameSession.pocketCarrots : 0;
	}
	
	private void savePlayerGems(jjPLAYER@ play) {
		gems[GEM::RED] = play.gems[GEM::RED];
		gems[GEM::GREEN] = play.gems[GEM::GREEN];
		gems[GEM::BLUE] = play.gems[GEM::BLUE];
		gems[GEM::PURPLE] = play.gems[GEM::PURPLE];
	}
}

class PurpleGem : RemovablePickup {

	PurpleGem(jjOBJ@ preset) {
		super(preset);
		preset.deactivates = false;
	}
	
	void onBehave(jjOBJ@ obj) override {
		obj.behave(BEHAVIOR::PICKUP);
	}
	
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) override {
		if (@player !is null) {
			currentGameSession.purpleGemsCollected++;
			fioDraw::doGemAnimation();
		}
		return RemovablePickup::onObjectHit(obj, bullet, player, force);
	}
}

class RemovableAmmo15 : jjBEHAVIORINTERFACE {
	
	RemovableAmmo15(jjOBJ@ preset) {
		preset.behavior = this;
	}
	
	void onBehave(jjOBJ@ obj) override {
		if (obj.state == STATE::ACTION) {
			removeFromEventMap(obj);
		}
		obj.behave(BEHAVIOR::AMMO15);
	}
}

class RemovableCrate : jjBEHAVIORINTERFACE {
	
	RemovableCrate(jjOBJ@ preset) {
		preset.behavior = this;
	}
	
	void onBehave(jjOBJ@ obj) override {
		if (obj.state == STATE::ACTION) {
			obj.behave(BEHAVIOR::CRATE);
			removeFromEventMap(obj);
		} else {
			obj.behave(BEHAVIOR::CRATE);
		}
	}
}

class RemovableEnemy : jjBEHAVIORINTERFACE {
	
	private jjBEHAVIOR nativeBehavior;
	
	RemovableEnemy(jjOBJ@ preset) {
		nativeBehavior = preset.behavior;
		preset.behavior = this;
	}
	
	void onBehave(jjOBJ@ obj) override {
		if (obj.state == STATE::KILL) {
			currentGameSession.enemiesSlain++;
			removeFromEventMap(obj);
		}
		obj.behave(nativeBehavior);
	}
	
	void onDraw(jjOBJ@ obj) {} // Required in order to override the drawing function in inherited class
}

class RemovableMonitor : jjBEHAVIORINTERFACE {
	
	RemovableMonitor(jjOBJ@ preset) {
		preset.behavior = this;
	}
	
	void onBehave(jjOBJ@ obj) override {
		if (obj.state == STATE::KILL) {
			obj.behave(BEHAVIOR::MONITOR);
			removeFromEventMap(obj);
		} else {
			obj.behave(BEHAVIOR::MONITOR);
		}
	}
}

class RemovablePickup : jjBEHAVIORINTERFACE {
	
	RemovablePickup(jjOBJ@ preset) {
		preset.behavior = this;
		preset.scriptedCollisions = true;
	}
	
	void onBehave(jjOBJ@ obj) override {
		obj.behave(BEHAVIOR::PICKUP);
	}
	
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (@player !is null && isPickable(obj)) {
			obj.scriptedCollisions = false;
			player.objectHit(obj, 0, HANDLING::PICKUP);
			removeFromEventMap(obj);
		}
		return true;
	}
}

class RingBall : jjBEHAVIORINTERFACE {

	jjOBJ@ obj;
	uint8 ballState = STATE_BALL_DEFAULT;
	int dieCounter = 0;
	int elapsedAttackInitiate = 24;
	SPRITE::Mode highlightType = SPRITE::SINGLEHUE;
	
	RingBall(jjOBJ@ obj) {
		@this.obj = obj;
		obj.behavior = this;
		obj.bulletHandling = HANDLING::DESTROYBULLET;
		obj.playerHandling = HANDLING::SPECIAL;
		obj.scriptedCollisions = true;
		
		dieCounter = 0;
		ballState = STATE_BALL_APPEAR;
		elapsedAttackInitiate = 24;
		highlightType = SPRITE::SINGLEHUE;
	}
	
	void onBehave(jjOBJ@ obj) override {
		switch (ballState) {
			case STATE_BALL_FALL:
			{
				obj.behave(BEHAVIOR::PICKUP, false);
				obj.playerHandling = HANDLING::DYING;
				
				if (dieCounter > 0) dieCounter--;
				else obj.delete();
			}
			break;
			case STATE_BALL_APPEAR:
			{
				obj.playerHandling = HANDLING::DYING;
				
				if (dieCounter < 255) dieCounter++;
				else ballState = STATE_BALL_DEFAULT;
			}
			break;
			default:
			{
				obj.playerHandling = HANDLING::SPECIAL;
			}
			break;
		}
	}
	void onDraw(jjOBJ@ obj) {
		switch (ballState) {
			case STATE_BALL_FALL:
			{
				jjDrawResizedSprite(obj.xPos, obj.yPos, ANIM::AMMO, 10, 0, 1.5, 1.5, SPRITE::BLEND_DISSOLVE, dieCounter, 1);
			}
			break;
			case STATE_BALL_APPEAR:
			{
				jjDrawResizedSprite(obj.xPos, obj.yPos, ANIM::AMMO, 10, 0, 1.5, 1.5, SPRITE::BLEND_DISSOLVE, dieCounter, 1);
			}
			break;
			default:
			{
				jjDrawResizedSprite(obj.xPos, obj.yPos, ANIM::AMMO, 10, 0, 1.5, 1.5, highlightType, elapsedAttackInitiate, 1);
			}
			break;
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) {
		if (@play != null) {
			play.hurt(1);
		}
		
		return true;
	}
}

// Note to self: A skeleton event needs to be placed in the actual level instead of loading the anims script-wise
// Otherwise weird behavior arises, like the skeleton objects being misplaced under the floor when there is no such
// object in the level and a new one is being added with jjAddObject for example
class Rabbit : jjBEHAVIORINTERFACE {

	// Named rabbitState to better distinguish it from jjOBJ::state property
	uint8 rabbitState = STATE_RABBIT_DEFAULT;
	
	void onBehave(jjOBJ@ obj) override {
		switch (obj.state) {
			case STATE::STILL:
			{
				obj.behave(BEHAVIOR::EVA);
			}
			break;
			case STATE::WALK:
			default:
			{
				obj.behave(BEHAVIOR::SKELETON);
			}
			break;
		}
	}
	void onDraw(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::STILL:
				drawRabbitStateStill(obj);
			break;
			case STATE::WALK:
			default:
				drawRabbitStateMoving(obj);
			break;
		}
	}
	
	private void determineStillAnimDefault(jjOBJ@ obj) {
		switch (play.charOrig) {
			case CHAR::LORI:
				obj.determineCurAnim(ANIM::LORI, RABBIT::ENDOFLEVEL);
				obj.frameID = 1;
				obj.xPos = obj.xOrg + 6;
				obj.yPos = obj.yOrg + 14;
				break;
			case CHAR::SPAZ:
				obj.determineCurAnim(ANIM::SPAZ, RABBIT::JUMPING3);
				obj.frameID = 2;
				obj.yPos = obj.yOrg + 8;
				break;
			case CHAR::JAZZ:
			default:
				obj.determineCurAnim(ANIM::JAZZ, RABBIT::JUMPING3);
				obj.frameID = 0;
				obj.xPos = obj.xOrg + 8;
				obj.yPos = obj.yOrg + 14;
				break;
		}
	}
	
	private void determineStillAnimFrightened(jjOBJ@ obj) {
		switch(play.charOrig) {
			case CHAR::LORI:
				obj.determineCurAnim(ANIM::LORI, RABBIT::DIVE);
				obj.frameID = 0;
				obj.xPos = obj.xOrg + 8;
				obj.yPos = obj.yOrg + 10;
				break;
			case CHAR::SPAZ:
				obj.determineCurAnim(ANIM::SPAZ, RABBIT::SKID3);
				obj.frameID = 0;
				obj.xPos = obj.xOrg + 4;
				break;
			case CHAR::JAZZ:
			default:
				obj.determineCurAnim(ANIM::JAZZ, RABBIT::ENDOFLEVEL);
				obj.frameID = 4;
				obj.xPos = obj.xOrg - 2;
				break;
		}
	}
	
	private void determineStillAnimWaiting(jjOBJ@ obj) {
		switch (play.charOrig) {
			case CHAR::LORI:
				obj.determineCurAnim(ANIM::LORI, RABBIT::IDLE1);
				obj.frameID = 7;
				break;
			case CHAR::SPAZ:
				obj.determineCurAnim(ANIM::SPAZ, RABBIT::IDLE2);
				obj.frameID = 0;
				break;
			case CHAR::JAZZ:
			default:
				obj.determineCurAnim(ANIM::JAZZ, RABBIT::IDLE1);
				obj.frameID = 2;
				break;
		}
	}
	
	private void drawRabbitStateMoving(jjOBJ@ obj) {
		switch (rabbitState) {
			case STATE_RABBIT_SKID:
			{
				switch (play.charOrig)
				{
					case CHAR::LORI:
						obj.determineCurAnim(ANIM::LORI, RABBIT::SKID3);
						break;
					case CHAR::SPAZ:
						obj.determineCurAnim(ANIM::SPAZ, RABBIT::SKID3);
						break;
					case CHAR::JAZZ:
					default:
						obj.determineCurAnim(ANIM::JAZZ, RABBIT::SKID3);
						break;
				}
			}
			break;
			case STATE_RABBIT_CONFUSED:
			{
				switch (play.charOrig) {
					case CHAR::LORI:
						obj.determineCurAnim(ANIM::LORI, RABBIT::LOOKUP - 1);
						break;
					case CHAR::SPAZ:
						obj.determineCurAnim(ANIM::SPAZ, RABBIT::LOOKUP - 1);
						break;
					case CHAR::JAZZ:
					default:
						obj.determineCurAnim(ANIM::JAZZ, RABBIT::LOOKUP - 1);
						break;
				}
			}
			break;
			case STATE_RABBIT_HURT:
			{
				switch (play.charOrig) {
					case CHAR::LORI:
						obj.determineCurAnim(ANIM::LORI, RABBIT::HURT);
						break;
					case CHAR::SPAZ:
						obj.determineCurAnim(ANIM::SPAZ, RABBIT::HURT);
						break;
					case CHAR::JAZZ:
					default:
						obj.determineCurAnim(ANIM::JAZZ, RABBIT::HURT);
						break;
				}
			}
			break;
			case STATE_RABBIT_TELEIN:
			{
				switch (play.charOrig) {
					case CHAR::LORI:
						obj.determineCurAnim(ANIM::LORI, RABBIT::TELEPORT);
						break;
					case CHAR::SPAZ:
						obj.determineCurAnim(ANIM::SPAZ, RABBIT::TELEPORT);
						break;
					case CHAR::JAZZ:
					default:
						obj.determineCurAnim(ANIM::JAZZ, RABBIT::TELEPORT);
						break;
				}
			}
			break;
			case STATE_RABBIT_TELEOUT:
			{
				switch (play.charOrig) {
					case CHAR::LORI:
						obj.determineCurAnim(ANIM::LORI, RABBIT::TELEPORTFALL);
						break;
					case CHAR::SPAZ:
						obj.determineCurAnim(ANIM::SPAZ, RABBIT::TELEPORTFALL);
						break;
					case CHAR::JAZZ:
					default:
						obj.determineCurAnim(ANIM::JAZZ, RABBIT::TELEPORTFALL);
						break;
				}
			}
			break;
			case STATE_RABBIT_FALL:
			{
				switch (play.charOrig) {
					case CHAR::LORI:
						obj.determineCurAnim(ANIM::LORI, RABBIT::TELEPORTFALLING);
						break;
					case CHAR::SPAZ:
						obj.determineCurAnim(ANIM::SPAZ, RABBIT::TELEPORTFALLING);
						break;
					case CHAR::JAZZ:
					default:
						obj.determineCurAnim(ANIM::JAZZ, RABBIT::TELEPORTFALLING);
						break;
				}
			}
			break;
			case STATE_RABBIT_RUN:
			default:
			{
				switch (play.charOrig)
				{
					case CHAR::LORI:
						obj.determineCurAnim(ANIM::LORI, RABBIT::RUN1);
						break;
					case CHAR::SPAZ:
						obj.determineCurAnim(ANIM::SPAZ, RABBIT::RUN1);
						break;
					case CHAR::JAZZ:
					default:
						obj.determineCurAnim(ANIM::JAZZ, RABBIT::RUN1);
						break;
				}
			}
			break;
		}
	}
	
	private void drawRabbitStateStill(jjOBJ@ obj) {
		switch (rabbitState) {
			case STATE_RABBIT_FRIGHTENED:
				determineStillAnimFrightened(obj);
				break;
			case STATE_RABBIT_WAITING:
				determineStillAnimWaiting(obj);
				break;
			case STATE_RABBIT_DEFAULT:
			default:
				determineStillAnimDefault(obj);
				break;
		}
	}
}

bool isCarrot(jjOBJ@ obj) {
	return obj.eventID == OBJECT::CARROT || obj.eventID == OBJECT::FULLENERGY;
}

bool isPickable(jjOBJ@ obj) {
	if (isCarrot(obj)) {
		return play.health < jjMaxHealth;
	}
	if (obj.eventID == OBJECT::BOUNCERAMMO3) {
		return play.ammo[WEAPON::BOUNCER] < MAXIMUM_AMMO;
	}
	if (obj.eventID == OBJECT::ICEAMMO3) {
		return play.ammo[WEAPON::ICE] < MAXIMUM_AMMO;
	}
	if (obj.eventID == OBJECT::SEEKERAMMO3) {
		return play.ammo[WEAPON::SEEKER] < MAXIMUM_AMMO;
	}
	if (obj.eventID == OBJECT::RFAMMO3) {
		return play.ammo[WEAPON::RF] < MAXIMUM_AMMO;
	}
	if (obj.eventID == OBJECT::TOASTERAMMO3) {
		return play.ammo[WEAPON::TOASTER] < MAXIMUM_AMMO;
	}
	if (obj.eventID == OBJECT::GUN8AMMO3) {
		return play.ammo[WEAPON::GUN8] < MAXIMUM_AMMO;
	}
	return true;
}

void removeFromEventMap(jjOBJ@ obj) {
	obj.eventID = 0;
	jjEventSet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0);
}