Downloads containing academy_entities.asc

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Academy Superjazz Battle N/A Download file

File preview

class AbstractBullet : jjBEHAVIORINTERFACE {
	private jjBEHAVIOR nativeBehavior;
	private HANDLING::Bullet nativeBulletHandling;
	private HANDLING::Player nativePlayerHandling;
    AbstractBullet(const jjBEHAVIOR &in setNativeBehavior,
			const HANDLING::Bullet &in setNativeBulletHandling,
			const HANDLING::Player &in setNativePlayerHandling) {
		nativeBehavior = setNativeBehavior;
		nativeBulletHandling = setNativeBulletHandling;
		nativePlayerHandling = setNativePlayerHandling;
	}
	void onBehave(jjOBJ@ obj) {
		initBullet(obj);
	}
	bool onIsRFBullet(jjOBJ@ obj) {
        return nativeBehavior == BEHAVIOR::RFBULLET;
    }
	void initBullet(jjOBJ@ obj) {
		int ammoCount = jjPlayers[obj.creatorID].ammo[WEAPON::BLASTER];
		obj.bulletHandling = HANDLING::IGNOREBULLET;
		obj.playerHandling = HANDLING::SPECIAL;
		if (debugModeOn) jjAlert("ammoCount: " + ammoCount);
		if (ammoCount >= BULLET_MAGIC_ARROW-1 && ammoCount <= BULLET_MAGIC_ARROW) {
			obj.scriptedCollisions = true;
			obj.behavior = MagicArrow();
		} else if (ammoCount >= BULLET_ICE_BOLT-1 && ammoCount <= BULLET_ICE_BOLT) {
			obj.scriptedCollisions = true;
			obj.behavior = IceBolt();
		} else if (ammoCount >= BULLET_FIREBALL-1 && ammoCount <= BULLET_FIREBALL) {
			obj.behavior = Fireball();
		} else if (fullFeatures) {
			obj.scriptedCollisions = true;
			obj.behavior = BulletWrapper(obj, nativeBehavior, nativePlayerHandling);
		} else {
			obj.behavior = nativeBehavior;
			obj.bulletHandling = nativeBulletHandling;
			obj.playerHandling = nativePlayerHandling;
		}
		jjPlayers[obj.creatorID].ammo[WEAPON::BLASTER] = 50;
	}
}

abstract class AreaOfEffect {
	float xPos;
	float yPos;
	float radius;
	int damage;
	uint elapsed;
	uint duration;
	jjPLAYER@ caster;
	AreaOfEffect(float setXPos, float setYPos, float setRadius, int setDamage, uint setDuration, jjPLAYER@ setCaster) {
		xPos = setXPos;
		yPos = setYPos;
		radius = setRadius;
		damage = setDamage;
		elapsed = duration = setDuration;
		@caster = setCaster;
	}
	void animate() {}
	void control(jjPLAYER@ play) {
		float scaledRadius = acUtils::getScaledRadius(radius, ANIM::FLARE, 5);
		float xDistance = play.xPos - xPos;
		float yDistance = play.yPos - yPos;
		if (xDistance*xDistance + yDistance*yDistance < scaledRadius*scaledRadius && acUtils::gameIsRunning()) {
			play.hurt(damage - players[play.playerID].magicResist, false, caster);
		}
	}
}

class Armageddon : AreaOfEffect {
	int frame;
	float meteorY;
	bool exploding;
	Armageddon(float setXPos, float setYPos, float setRadius, int setDamage, uint setDuration, jjPLAYER@ setCaster) {
		super(setXPos, setYPos, setRadius, setDamage, setDuration, setCaster);
		meteorY = setYPos - 640;
		exploding = false;
		frame = 4;
	}
	void animate() override {
		if (exploding) {
			jjDrawResizedSprite(xPos, yPos, ANIM::AMMO, 81, frame, 14, 14, SPRITE::NORMAL, 0, 1);
			if (jjGameTicks % 7 == 0) {
				if (frame < 11) {
					frame++;
				}
			}
		} else {
			jjDrawRotatedSprite(xPos, meteorY, ANIM::ROCK, 0, 0, 0, 5, 5, SPRITE::TINTED, 24, 1);
		}
		if (meteorY < yPos) {
			meteorY += 32;
		} else if (!exploding) {
			exploding = true;
			jjSamplePriority(SOUND::INTRO_BOEM2);
		}
	}
	void control(jjPLAYER@ play) override {
		if (playerInRange(play, true) && exploding) {
			play.hurt(damage - players[play.playerID].magicResist, false, caster);
		}
	}
	bool playerInRange(jjPLAYER@ play, bool vulnerable) {
		float scaledRadius = acUtils::getScaledRadius(radius, ANIM::FLARE, 5);
		float xDistance = play.xPos - xPos;
		float yDistance = play.yPos - yPos;
		if (vulnerable) {
			return xDistance*xDistance + yDistance*yDistance < scaledRadius*scaledRadius && acUtils::gameIsRunning()
				&& acUtils::checkIfPlayerIsVulnerable(caster, play, cast<Spell@>(spells["A"]));
		}
		return xDistance*xDistance + yDistance*yDistance < scaledRadius*scaledRadius;
	}
}

class Bless : Effect {
	Bless(jjPLAYER@ setPlay, string setName, SPELL setEnumValue, SPELL setCounterEffect,
			uint setDuration) {
		super(setPlay, setName, setEnumValue, setCounterEffect, setDuration);
	}
	void affect() override {}
}

class BloodLust : Effect {
	int originalFastFire;
	BloodLust(jjPLAYER@ setPlay, string setName, SPELL setEnumValue, SPELL setCounterEffect,
			uint setDuration, bool setIsLocal) {
		super(setPlay, setName, setEnumValue, setCounterEffect, setDuration, setIsLocal);
		originalFastFire = setPlay.fastfire;
	}
	~BloodLust() {
		play.fastfire = originalFastFire;
	}
	void affect() override {
		play.fastfire = 6;
	}
}

class BulletWrapper : jjBEHAVIORINTERFACE {
    private jjBEHAVIOR nativeBehavior;
	private HANDLING::Player nativePlayerHandling;
    BulletWrapper(jjOBJ@ obj, const jjBEHAVIOR &in setNativeBehavior, const HANDLING::Player &in setNativeHandling) {
		nativeBehavior = setNativeBehavior;
		nativePlayerHandling = setNativeHandling;
	}
    void onBehave(jjOBJ@ obj) {
		if (obj.state == STATE::KILL) {
			obj.delete();
		}
		else {
			controlObjectCollision(obj);
			obj.behave(nativeBehavior);
		}
    }
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) {
		if (@play != null && acUtils::gameIsRunning()) {
			jjPLAYER@ creator = jjPlayers[obj.creatorID];
			if (obj.creatorType == CREATOR::PLAYER && creator.isEnemy(play) &&
					play.blink == 0 && creator.blink == 0) {
				int8 damage = obj.var[6] & 8 != 0 ? 2 : 1;
				if (jjPlayers[creator.playerID].isZombie) damage++;
				if (debugModeOn) jjAlert("damage now: " + damage);
				
				damage += acUtils::getDamageModifier(players[creator.playerID], players[play.playerID]);
				
				if (damage < 0) damage = 0;
				if (debugModeOn) jjAlert("damage then: " + damage);
				
				play.hurt(damage, false, creator);
				obj.state = STATE::KILL;
			}
			return true;
		}
		return false;
	}
}

class ChainLightningTarget {
	int8 id;
	int8 parentID;
	int8 playerID;
	int8 damage;
	uint elapsed;
	uint duration;
	ChainLightningTarget(int8 setID, int8 setPlayerID, int8 setDamage, uint setDuration, int8 setParentID = -1) {
		id = setID;
		playerID = setPlayerID;
		damage = setDamage;
		duration = setDuration;
		elapsed = setDuration;
		parentID = setParentID;
	}
}

class Chunk {
	array<jjOBJ@> objects;
	void addObject(jjOBJ@ obj) {
		objects.insertLast(obj);
	}
	void clearObjects() {
		objects.removeRange(0, objects.length);
	}
}

class DeathRipple : AreaOfEffect {
	int frame;
	float range;
	DeathRipple(float setXPos, float setYPos, float setRadius, int setDamage, uint setDuration, jjPLAYER@ setCaster) {
		super(setXPos, setYPos, setRadius, setDamage, setDuration, setCaster);
		frame = 0;
	}
	void animate() override {
		for (int i = 0; i < 32; i++) {
			jjPLAYER@ play = jjPlayers[i];
			if (playerInRange(play, false)) {
				jjDrawRotatedSprite(play.xPos, play.yPos, ANIM::AMMO, 66, frame, 0, 2, 2, SPRITE::SINGLECOLOR, 24);
			}
		}
		if (jjGameTicks % 5 == 0) {
			if (frame < 8) {
				frame++;
			} else {
				frame = 0;
			}
		}
	}
	void control(jjPLAYER@ play) override {
		if (playerInRange(play, true)) {
			play.hurt(damage - players[play.playerID].magicResist, false, caster);
		}
	}
	bool playerInRange(jjPLAYER@ play, bool vulnerable) {
		float scaledRadius = acUtils::getScaledRadius(radius, ANIM::FLARE, 5);
		float xDistance = play.xPos - xPos;
		float yDistance = play.yPos - yPos;
		if (vulnerable) {
			return xDistance*xDistance + yDistance*yDistance < scaledRadius*scaledRadius && acUtils::gameIsRunning()
				&& acUtils::checkIfPlayerIsVulnerable(caster, play, cast<Spell@>(spells["H"]));
		}
		return xDistance*xDistance + yDistance*yDistance < scaledRadius*scaledRadius;
	}
}

class DisruptingRay : Effect {
	DisruptingRay(jjPLAYER@ setPlay, string setName, SPELL setEnumValue, SPELL setCounterEffect,
			uint setDuration, bool setIsLocal, bool setIsCurse) {
		super(setPlay, setName, setEnumValue, setCounterEffect, setDuration, setIsLocal, setIsCurse);
	}
	void affect() override {}
}

abstract class Effect {
	bool isLocal;
	bool isCurse;
	jjPLAYER@ play;
	string name;
	SPELL enumValue;
	SPELL counterEffect;
	uint elapsed;
	uint duration;
	Effect() {}
	Effect(jjPLAYER@ setPlay, string setName, SPELL setEnumValue, SPELL setCounterEffect, uint setDuration,
			bool setIsLocal = false, bool setIsCurse = false) {
		@play = setPlay;
		name = setName;
		enumValue = setEnumValue;
		counterEffect = setCounterEffect;
		duration = setDuration;
		isLocal = setIsLocal;
		isCurse = setIsCurse;
		elapsed = setDuration;
	}
	void affect() {}
}

class Fireball : jjBEHAVIORINTERFACE {
	uint8 frame;
	jjPLAYER@ currentCaster;
	jjPLAYER@ originalCaster;
	Fireball() {
		frame = 0;
	}
	void onBehave(jjOBJ@ obj) {
		switch(obj.state) {
			case STATE::START:
				jjSample(obj.xPos, obj.yPos, SOUND::AMMO_FIREGUN1A, 0, 7500);
				obj.direction = jjPlayers[obj.creatorID].direction;
				obj.xPos += obj.direction >= 0 ? TILE : -TILE;
				obj.state = STATE::FLY;
				obj.light = 125;
				@currentCaster = jjPlayers[obj.creatorID];
				@originalCaster = jjPlayers[obj.creatorID];
				break;
			case STATE::FLY:
			{
				for (int i = 0; i < 32; i++) {
					jjPLAYER@ play = jjPlayers[i];
					if (obj.doesCollide(play, true) && play !is currentCaster) {
						if (players[play.playerID].hasEffect(SPELL_MAGIC_MIRROR)) {
							acUtils::addMagicMirrorAnimation(play.playerID, -obj.direction);
							obj.direction = -obj.direction;
							@currentCaster = play;
						} else {
							explode(obj);
						}
					}
				}
				obj.xPos += obj.direction >= 0 ? 10 : -10;
				if (obj.xPos < 0 || obj.xPos > jjLayerWidth[4] * TILE) {
					obj.delete();
				} else if ((jjLayers[4].maskedVLine(int(obj.xPos+TILE), int(obj.yPos), 1))
						|| (jjLayers[4].maskedVLine(int(obj.xPos-TILE), int(obj.yPos), 1))) {
					explode(obj);
				}
			}
			break;
		}
	}
	void onDraw(jjOBJ@ obj) {
		int angle = obj.direction >= 0 ? 0 : 540;
		jjDrawRotatedSprite(obj.xPos, obj.yPos, ANIM::AMMO, 14, frame, angle, 2, 2);
		if (frame < 7) frame++;
		else frame = 0;
	}
	void explode(jjOBJ@ obj) {
		acSpells::doAOE(currentCaster, originalCaster, SPELL_FIREBALL, obj.xPos, obj.yPos);
		obj.delete();
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		return false;
	}
}

class FireballExplosion : AreaOfEffect {
	int frame;
	FireballExplosion(float setXPos, float setYPos, float setRadius, int setDamage, uint setDuration, jjPLAYER@ setCaster) {
		super(setXPos, setYPos, setRadius, setDamage, setDuration, setCaster);
		frame = 0;
	}
	void animate() override {
		jjDrawResizedSprite(xPos, yPos, ANIM::AMMO, 81, frame, radius, radius, SPRITE::NORMAL, 0, 1);
		if (jjGameTicks % 7 == 0) {
			if (frame < 11) {
				frame++;
			}
		}
	}
	void control(jjPLAYER@ play) override {
		float scaledRadius = acUtils::getScaledRadius(radius, ANIM::AMMO, frame, 81);
		float xDistance = play.xPos - xPos;
		float yDistance = play.yPos - yPos;
		if (xDistance*xDistance + yDistance*yDistance < scaledRadius*scaledRadius && acUtils::gameIsRunning()) {
			play.hurt(damage - players[play.playerID].magicResist, false, caster);
		}
	}
}

class Forgetfulness : Effect {
	Forgetfulness(jjPLAYER@ setPlay, string setName, SPELL setEnumValue, SPELL setCounterEffect,
			uint setDuration, bool setIsLocal, bool setIsCurse) {
		super(setPlay, setName, setEnumValue, setCounterEffect, setDuration, setIsLocal, setIsCurse);
	}
	void affect() override {
		play.noFire = true;
	}
}

class Frenzy : Effect {
	Frenzy(jjPLAYER@ setPlay, string setName, SPELL setEnumValue, SPELL setCounterEffect,
			uint setDuration) {
		super(setPlay, setName, setEnumValue, setCounterEffect, setDuration);
	}
	void affect() override {}
}

class FrostRing : AreaOfEffect {
	int frame;
	FrostRing(float setXPos, float setYPos, float setRadius, int setDamage, uint setDuration, jjPLAYER@ setCaster) {
		super(setXPos, setYPos, setRadius, setDamage, setDuration, setCaster);
		frame = 0;
	}
	void animate() override {
		jjDrawResizedSprite(xPos, yPos, ANIM::AMMO, 82, frame, radius, radius, SPRITE::TINTED, 32, 1);
		if (jjGameTicks % 3 == 0) {
			if (frame < 11) {
				frame++;
			}
		}
	}
	void control(jjPLAYER@ play) override {
		float scaledRadius = acUtils::getScaledRadius(radius, ANIM::AMMO, frame, 82);
		float xDistance = play.xPos - xPos;
		float yDistance = play.yPos - yPos;
		if (xDistance*xDistance + yDistance*yDistance < scaledRadius*scaledRadius && acUtils::gameIsRunning()
				&& play !is caster) {
			play.hurt(damage - players[play.playerID].magicResist, false, caster);
			play.freeze();
		}
	}
}

class GemMine {
	int8 id;
	int8 ownerID;
	int captureElapsed, lockElapsed, yieldElapsed;
	int captureBegin = 350;
	int lockBegin = 2100;
	int top, bottom, left, right;
	int yieldBegin = 350;
	array<int8> playersInRange;
	string name;
	GemMine(int8 id, string name, int top, int bottom, int left, int right) {
		this.id = id;
		this.name = name;
		this.top = top;
		this.bottom = bottom;
		this.left = left;
		this.right = right;
		ownerID = -1;
		captureElapsed = captureBegin;
		yieldElapsed = yieldBegin;
		lockElapsed = lockBegin;
	}
	void capture(int8 setOwnerID) {
		captureElapsed = captureBegin;
		lockElapsed = lockBegin;
		ownerID = setOwnerID;
	}
	void control(jjPLAYER@ localPlayer, Player@ asPlayer) {
		if (acUtils::gameIsRunning()) {
			if (lockElapsed > 0) {
				lockElapsed--;
			} else {
				for (int8 i = 0; i < 32; i++) {
					jjPLAYER@ play = jjPlayers[i];
					int index = playersInRange.find(play.playerID);
					if (index < 0 && playerInRange(play)) {
						playersInRange.insertLast(play.playerID);
					} else if (index >= 0 && !playerInRange(play)) {
						playersInRange.removeAt(index);
					}
				}
				if (playerInRange(localPlayer) && !asPlayer.isDead
						&& localPlayer.playerID != ownerID && playersInRange.length == 1) {
					if (captureElapsed > 0) {
						if (captureElapsed == captureBegin) {
							jjAlert("Capturing gem mine in..." + captureElapsed / SECOND, false, STRING::MEDIUM);
						} else if (captureElapsed % SECOND == 0) {
							jjAlert("" + captureElapsed / SECOND, false, STRING::MEDIUM);
						}
						captureElapsed--;
					} else {
						capture(localPlayer.playerID);
						acNetworking::sendCaptureGemMinePacket(localPlayer.playerID, id);
					}
				} else {
					captureElapsed = captureBegin;
				}
			}
			if (ownerID >= 0 && jjPlayers[ownerID].isLocal) {
				yieldGem(jjPlayers[ownerID]);
			} 
		}
	}
	void yieldGem(jjPLAYER@ play) {
		if (yieldElapsed > 0) {
			yieldElapsed--;
		} else {
			yieldElapsed = yieldBegin;
			jjAddObject(OBJECT::PURPLEGEM, play.xPos, play.yPos);
		}
	}
	void draw(jjCANVAS@ canvas) {
		uint8 color = 0;
		uint8 _ = 0;
		string ownerName = "";
		if (ownerID >= 0) {
			jjPLAYER@ owner = jjPlayers[ownerID];
			owner.furGet(_, color, _, _);
			ownerName = owner.name;
		}
		jjTEXTAPPEARANCE centeredText();
		centeredText.align = STRING::CENTER;
		canvas.drawSprite(right * TILE, bottom * TILE, ANIM::FLAG, 3, 0, 0, SPRITE::SINGLECOLOR, color);
		canvas.drawString(right * TILE, (bottom + 1) * TILE, "Owned by\n" + ownerName, STRING::SMALL, centeredText);
		if (acUtils::gameIsRunning() && lockElapsed > 0) {
			canvas.drawString(right * TILE, (bottom + 1) * TILE, "\n\nUnlocking in " + int(lockElapsed / SECOND), STRING::SMALL, centeredText);
		}
	}
	bool playerInRange(jjPLAYER@ play) {
		return play.xPos >= left * TILE && play.xPos <= right * TILE
				&& play.yPos >= top * TILE && play.yPos <= bottom * TILE;
	}
}

class Key {
	int keyCode;
	string key;
	bool keyPressed;
	Key() {}
	Key(int setKeyCode, string setKey) {
		keyCode = setKeyCode;
		key = setKey;
		keyPressed = false;
	}
}

class IceBolt : jjBEHAVIORINTERFACE {
	int frame;
	jjPLAYER@ currentCaster;
	jjPLAYER@ originalCaster;
	IceBolt() {
		frame = 0;
	}
	void onBehave(jjOBJ@ obj) {
		switch(obj.state) {
			case STATE::START:
				jjSample(obj.xPos, obj.yPos, SOUND::AMMO_ICEPU4);
				obj.direction = jjPlayers[obj.creatorID].direction;
				obj.xPos += obj.direction >= 0 ? 16 : -16;
				obj.state = STATE::FLY;
				obj.var[6] = 16;
				obj.light = 100;
				@currentCaster = jjPlayers[obj.creatorID];
				@originalCaster = jjPlayers[obj.creatorID];
				break;
			case STATE::FLY:
				for (int i = 0; i < 32; i++) {
					jjPLAYER@ play = jjPlayers[i];
					if (abs(obj.xPos - play.xPos) <= 32 && abs(obj.yPos - play.yPos) <= 32) {
						if (obj.doesCollide(play, true) && currentCaster.isEnemy(play)
								&& acUtils::gameIsRunning()) {
							if (players[play.playerID].hasEffect(SPELL_MAGIC_MIRROR)) {
								acUtils::addMagicMirrorAnimation(play.playerID, -obj.direction);
								obj.direction = -obj.direction;
								@currentCaster = play;
							} else {
								Spell@ spell = cast<Spell@>(spells["E"]);
								play.hurt(spell.baseDamage
										+ players[originalCaster.playerID].spellDamageBonus
										- players[play.playerID].magicResist,
										false, currentCaster);
								play.freeze();
							}
						}
					}
				}
				obj.xPos += obj.direction >= 0 ? 10 : -10;
				if (obj.xPos < 0 || obj.xPos > jjLayerWidth[4] * TILE) {
					obj.delete();
				}
				controlObjectCollision(obj);
				break;
			case STATE::KILL:
				obj.state = STATE::FLY;
				break;
			case STATE::EXPLODE:
			default:
				obj.state = STATE::FLY;
				break;
		}
	}
	void onDraw(jjOBJ@ obj) {
		int angle = obj.direction >= 0 ? 0 : 540;
		jjDrawRotatedSprite(obj.xPos, obj.yPos, ANIM::AMMO, 11, frame, angle, 2, 2, SPRITE::TINTED, 35);
		
		if (frame < 7) frame++;
		else frame = 0;
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		return false;
	}
}

class Implosion : AreaOfEffect {
	float scale;
	int8[] targets;
	bool finished;
	Implosion(float setXPos, float setYPos, float setRadius, int setDamage, uint setDuration, jjPLAYER@ setCaster) {
		super(setXPos, setYPos, setRadius, setDamage, setDuration, setCaster);
		scale = 5;
		finished = false;
		
		for (int i = 0; i < 32; i++) {
			jjPLAYER@ play = jjPlayers[i];
			float scaledRadius = acUtils::getScaledRadius(radius, ANIM::FLARE, 5);
			Spell@ spell = cast<Spell@>(spells["Q"]);
			
			if (acUtils::checkIfPlayerIsInRange(setCaster, play, scaledRadius) && acUtils::checkIfPlayerIsVulnerable(caster, play, spell)
					&& acUtils::gameIsRunning() && targets.find(play.playerID) < 0) {
				if (debugModeOn) jjAlert("" + play.playerID);
				targets.insertLast(play.playerID);
			}
		}
	}
	void animate() override {
		for (uint i = 0; i < targets.length; i++) {
			jjPLAYER@ play = jjPlayers[targets[i]];
			jjDrawRotatedSprite(play.xPos, play.yPos, ANIM::BOLLPLAT, 0, 0, 0, scale, scale, SPRITE::SINGLECOLOR, 1);
		}
		if (scale > 0) {
			scale -= 0.1;
		} else if (!finished) {
			finished = true;
			for (uint i = 0; i < targets.length; i++) {
				jjPLAYER@ play = jjPlayers[targets[i]];
				
				if (play.isLocal) {
					play.hurt(damage - players[play.playerID].magicResist, true, caster);
					jjSamplePriority(SOUND::SMALTREE_GROUND);
				}
			}
		}
	}
	void control(jjPLAYER@ play) override {}
	bool playerInRange(jjPLAYER@ play, bool vulnerable) {
		float scaledRadius = acUtils::getScaledRadius(radius, ANIM::FLARE, 5);
		float xDistance = play.xPos - xPos;
		float yDistance = play.yPos - yPos;
		
		if (vulnerable) {
			return xDistance*xDistance + yDistance*yDistance < scaledRadius*scaledRadius && acUtils::gameIsRunning()
				&& acUtils::checkIfPlayerIsVulnerable(caster, play, cast<Spell@>(spells["Q"]));
		}
		return xDistance*xDistance + yDistance*yDistance < scaledRadius*scaledRadius;
	}
}

class MagicArrow : jjBEHAVIORINTERFACE {
	jjPLAYER@ currentCaster;
	jjPLAYER@ originalCaster;
	void onBehave(jjOBJ@ obj) {
		switch(obj.state) {
			case STATE::START:
				jjSample(obj.xPos, obj.yPos, SOUND::AMMO_BULFL1);
				obj.direction = jjPlayers[obj.creatorID].direction;
				obj.xPos += obj.direction >= 0 ? 16 : -16;
				obj.state = STATE::FLY;
				obj.var[6] = 16;
				obj.light = 100;
				@currentCaster = jjPlayers[obj.creatorID];
				@originalCaster = jjPlayers[obj.creatorID];
				break;
			case STATE::FLY:
			{
				for (int i = 0; i < 32; i++) {
					jjPLAYER@ play = jjPlayers[i];
					if (abs(obj.xPos - play.xPos) <= 32 && abs(obj.yPos - play.yPos) <= 32) {
						if (obj.doesCollide(play, true) && currentCaster.isEnemy(play)
								&& acUtils::gameIsRunning()) {
							if (players[play.playerID].hasEffect(SPELL_MAGIC_MIRROR)) {
								acUtils::addMagicMirrorAnimation(play.playerID, -obj.direction);
								obj.direction = -obj.direction;
								@currentCaster = play;
							} else {
								Spell@ spell = cast<Spell@>(spells["M"]);
								play.hurt(spell.baseDamage
										+ players[originalCaster.playerID].spellDamageBonus
										- players[play.playerID].magicResist,
										false, currentCaster);
							}
						}
					}
				}
				obj.xPos += obj.direction >= 0 ? 10 : -10;
				obj.yPos += jjSin(8 * uint(obj.xPos % float(1024))) * 8;
				if (obj.xPos < 0 || obj.xPos > jjLayerWidth[4] * TILE) {
					obj.delete();
				}
				controlObjectCollision(obj);
			}
			break;
			case STATE::KILL:
				obj.state = STATE::FLY;
				break;
			case STATE::EXPLODE:
			default:
				obj.state = STATE::FLY;
				break;
		}
	}
	void onDraw(jjOBJ@ obj) {
		int angle = obj.direction >= 0 ? 950 : 425;
		jjDrawRotatedSprite(obj.xPos, obj.yPos, ANIM::FLAG, 0, 0, angle, 1.5, 1.5, SPRITE::MENUPLAYER);
		obj.particlePixelExplosion(1);
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		return true;
	}
}

class MagicMirror : Effect {
	MagicMirror(jjPLAYER@ setPlay, string setName, SPELL setEnumValue, SPELL setCounterEffect,
			uint setDuration) {
		super(setPlay, setName, setEnumValue, setCounterEffect, setDuration);
	}
	void affect() override {}
}

class MagicMirrorAnimation {
	int direction;
	int frameCounter;
	int elapsed;
	jjPLAYER@ play;
	MagicMirrorAnimation(jjPLAYER@ play, int direction) {
		@this.play = play;
		this.direction = direction;
		elapsed = 37;
		frameCounter = 0;
	}
	bool draw() {
		int angle = direction >= 0 ? 256 : 768;
		int xOffset = direction >= 0 ? 24 : -24;
		jjDrawRotatedSprite(play.xPos + xOffset, play.yPos, ANIM::AMMO, 76, frameCounter, 256, 1.5, 1.5,
				SPRITE::SINGLEHUE, 128);
		if (elapsed > 0) {
			elapsed--;
			frameCounter++;
			return false;
		} else {
			return true;
		}
	}
}

class Player {
	int mana;
	int maxMana;
	int manaRegenCounter;
	int manaRegenCounterEnd = 140;
	int manaRegenRate;
	int cooldown;
	int8 playerID;
	int8 magicResist;
	int8 spellDamageBonus;
	uint32 originalFur;
	uint8 currentHealth;
	uint spellDurationBonus;
	bool isDead;
	bool isChanneling;
	string selectedSpellKey;
	array<SKILL> activeSkills;
	array<Effect@> activeEffects;
	Player() {
		isDead = false;
		isChanneling = false;
		selectedSpellKey = "";
		mana = STARTING_MANA;
		maxMana = DEFAULT_MAX_MANA;
		manaRegenCounter = 0;
		manaRegenRate = DEFAULT_MANA_REGEN_RATE;
		playerID = 0;
		cooldown = 0;
		magicResist = 0;
		spellDamageBonus = 0;
		spellDurationBonus = 0;
	}
	~Player() {
		jjPLAYER@ play = jjPlayers[playerID];
		if (originalFur != 0 && !play.isZombie) {
			play.fur = originalFur;
		}
	}
	void addNewRandomSkill(string playerName) {
		if (activeSkills.length < skills.length) {
			int index = 0;
			SKILL randomSkill = SKILL_MAX_MANA;
			while (index >= 0) {
				uint random = jjRandom() % skills.length;
				randomSkill = SKILL(random);
				index = activeSkills.find(randomSkill);
			}
			if (jjIsServer) {
				acUtils::alertOfLearnedSkill(playerName, randomSkill);
			} else {
				acNetworking::sendSkillLearnedPacket(playerID, randomSkill);
			}
			acUtils::setSkillBonus(this, randomSkill, playerID);
			activeSkills.insertLast(randomSkill);
		}
	}
	bool isExistingEffectOrCounterEffect(Effect@ activeEffect, Effect@ newEffect) {
		return activeEffect.enumValue == newEffect.enumValue
				|| activeEffect.enumValue == newEffect.counterEffect;
	}
	void setNewActiveEffect(Effect@ newEffect) {
		int j = 0;
		int activeEffectsLength = activeEffects.length();
		for (int i = 0; i < activeEffectsLength; i++) {
			if (!isExistingEffectOrCounterEffect(activeEffects[i], newEffect)) {
				@activeEffects[j] = activeEffects[i];
				j++;
			}
		}
		activeEffects.removeRange(j, activeEffectsLength - j);
		activeEffects.insertLast(newEffect);
	}
	void setSelectedSpellKey(string newSelectedSpellKey) {
		selectedSpellKey = newSelectedSpellKey;
	}
	void setChanneledSpellKey(string newSelectedSpellKey) {
		selectedSpellKey = newSelectedSpellKey;
		isChanneling = true;
	}
	void unSetChanneledSpellKey() {
		selectedSpellKey = "";
		isChanneling = false;
	}
	void removeActiveEffect(uint index) {
		activeEffects.removeAt(index);
	}
	void removeAllActiveEffects() {
		activeEffects.removeRange(0, activeEffects.length);
	}
	void removeNegativeEffects() {
		int j = 0;
		int activeEffectsLength = activeEffects.length();
		for (int i = 0; i < activeEffectsLength; i++) {
			if (!activeEffects[i].isCurse) {
				@activeEffects[j] = activeEffects[i];
				j++;
			}
		}
		activeEffects.removeRange(j, activeEffectsLength - j);
	}
	void regenerateMana(jjPLAYER@ play) {
		if (manaRegenCounter < manaRegenCounterEnd) {
			manaRegenCounter += isInMagicWell(play) ? manaRegenRate * 4 : manaRegenRate;
		} else {
			manaRegenCounter = 0;
			if (mana < maxMana) {
				mana++;
			}
		}
	}
	bool hasEffect(SPELL effectEnum) {
		for (uint i = 0; i < activeEffects.length; i++) {
			if (activeEffects[i].enumValue == effectEnum) {
				return true;
			}
		}
		return false;
	}
	bool isInMagicWell(jjPLAYER@ play) {
		return play.xPos > MAGIC_WELL_LEFT * TILE && play.xPos < MAGIC_WELL_RIGHT * TILE
				&& play.yPos > MAGIC_WELL_TOP * TILE;
	}
}

class Precision : Effect {
	Precision(jjPLAYER@ setPlay, string setName, SPELL setEnumValue, SPELL setCounterEffect,
			uint setDuration) {
		super(setPlay, setName, setEnumValue, setCounterEffect, setDuration);
	}
	void affect() override {}
}

class Skill {
	SKILL enumValue;
	string name;
	string description;
	Skill(SKILL setEnumValue) {
		enumValue = setEnumValue;
	}
	Skill(SKILL setEnumValue, string setName, string setDescription) {
		enumValue = setEnumValue;
		name = setName;
		description = setDescription;
	}
	bool opEquals(Skill@ other) {
		return enumValue == other.enumValue;
	}
}

class Slow : Effect {
	Slow(jjPLAYER@ setPlay, string setName, SPELL setEnumValue, SPELL setCounterEffect,
			uint setDuration, bool setIsLocal, bool setIsCurse) {
		super(setPlay, setName, setEnumValue, setCounterEffect, setDuration, setIsLocal, setIsCurse);
	}
	void affect() override {
		if (play.xSpeed > SLOW_SPEED) {
			play.xSpeed = SLOW_SPEED;
		} else if (play.xSpeed < -SLOW_SPEED) {
			play.xSpeed = -SLOW_SPEED;
		}
	}
}

class Spell {
	string key;
	string name;
	SPELL enumValue;
	SPELL counterSpell;
	uint tier;
	uint numpad;
	int localCount;
	int baseDamage;
	uint baseDuration;
	int baseManaCost;
	float channelingTime;
	float radius;
	bool damagesAll;
	string description;
	Spell() {}
	Spell(string setKey, string setName, SPELL setEnumValue, SPELL setCounterSpell, uint setTier,
			uint setNumpad, int setBaseDamage, uint setBaseDuration, int setBaseManaCost,
			float setChannelingTime, float setRadius, bool setDamagesSelf, string setDescription) {
		key = setKey;
		name = setName;
		enumValue = setEnumValue;
		counterSpell = setCounterSpell;
		tier = setTier;
		numpad = setNumpad;
		baseDamage = setBaseDamage;
		baseDuration = setBaseDuration;
		baseManaCost = setBaseManaCost;
		channelingTime = setChannelingTime;
		radius = setRadius;
		damagesAll = setDamagesSelf;
		description = setDescription;
		
		localCount = 0;
	}
	int opCmp(const Spell@ otherSpell) {
		return numpad - otherSpell.numpad;
	}
}

class StoneSkin : Effect {
	StoneSkin(jjPLAYER@ setPlay, string setName, SPELL setEnumValue, SPELL setCounterEffect,
			uint setDuration) {
		super(setPlay, setName, setEnumValue, setCounterEffect, setDuration);
	}
	void affect() override {}
}

class VisualGem {
	float x, y, scale;
	int angle, color, frame;
	VisualGem(float setX, float setY) {
		x = setX;
		y = setY;
		angle = jjRandom() % 1024;
		color = jjRandom() % 12;
		//scale = jjRandom() % 40 * 0.01 + 0.1;
		scale = 0.3;
		frame = jjRandom() % 8;
	}
	void draw(jjCANVAS@ canvas) {
		canvas.drawRotatedSprite(int(x*TILE), int(y*TILE), ANIM::PICKUPS, 34, frame, angle, scale, scale, SPRITE::GEM, color);
	}
}

class WallOfFire {
	int damage;
	int frame;
	int channel;
	int8 height;
	float xOrigin;
	float yOrigin;
	uint elapsed;
	uint duration;
	jjPLAYER@ caster;
	WallOfFire(float setXOrigin, float setYOrigin, int8 setHeight, int setDamage, uint setDuration, jjPLAYER@ setCaster) {
		xOrigin = setXOrigin;
		yOrigin = setYOrigin;
		height = setHeight;
		damage = setDamage;
		elapsed = duration = setDuration;
		@caster = setCaster;
		frame = 1;
		channel = 0;
		if (height > WALL_OF_FIRE_MAX_HEIGHT) {
			height = WALL_OF_FIRE_MAX_HEIGHT;
		}
	}
}

class Weakness : Effect {
	Weakness(jjPLAYER@ setPlay, string setName, SPELL setEnumValue, SPELL setCounterEffect,
			uint setDuration, bool setIsLocal, bool setIsCurse) {
		super(setPlay, setName, setEnumValue, setCounterEffect, setDuration, setIsLocal, setIsCurse);
	}
	void affect() override {}
}

void controlObjectCollision(jjOBJ@ obj) {
	if (obj.state != STATE::EXPLODE) {
		uint yChunk = (uint(obj.yPos) + 64) >> 7;
		uint xChunk = (uint(obj.xPos) + 64) >> 7;
		for (uint y = 0; y < 2; y++) {
			for (uint x = 0; x < 2; x++) {
				if (yChunk - y < chunks.length && xChunk - x < chunks[yChunk - y].length) {
					array<jjOBJ@>@ objects = chunks[yChunk - y][xChunk - x].objects;
					for (uint i = 0; i < objects.length; i++) {
						jjOBJ@ other = objects[i];
						if (other.playerHandling == HANDLING::PICKUP && obj.doesCollide(other, true)) {
							obj.objectHit(other, HANDLING::PICKUP);
						} else if (other.playerHandling == HANDLING::SPECIAL && obj.doesCollide(other, true)) {
							obj.objectHit(other, HANDLING::ENEMY);
						} else if (other.eventID == OBJECT::DESTRUCTSCENERY && obj.doesCollide(other, true)) {
							obj.objectHit(other, HANDLING::ENEMY);
							obj.delete();
						}
					}
				}
			}
		}
	}
}