Downloads containing xlmlockjaw.j2as

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

File preview

const bool MLLESetupSuccessful = MLLE::Setup(array<MLLEWeaponApply@> = {null, null, null, null, null, null, null, se::BuzzsawerMLLEWrapper(), se::FireworkMLLEWrapper()}); ///@MLLE-Generated
#pragma description "Far on the edges of Shellian Empire-controlled space, in an unremarkable solar system, there is the gas giant Chelonia. Unbeknownst to anyone, not even the Emperor's closest subordinates, the fourth moon of this planet is the location of his secret hideout: an imposing fortress built in a harsh volcanic environment, under atmosphere so thin that it's a miracle it's breathable. But in the vast galaxy full of life, no secret can truly remain undiscovered. A pirate band of Carrotusians has stumbled upon this place by chance, and found it to be the perfect spot for an illegal battle arena. Prepare for brutal weapons unsanctioned by the Sports Combat Committee, deadly pools of lava, and a vicious crusher trap designed by Dark Shell himself! Are you tough enough to try your might in a battle match beyond the laws of both the Carrotus Kingdom and the Shellian Empire?"
#include "MLLE-Include-1.8w.asc" ///@MLLE-Generated
#pragma require "xlmlockjaw-MLLE-Data-2.j2l" ///@MLLE-Generated
#pragma require "xlmlockjaw-MLLE-Data-1.j2l" ///@MLLE-Generated
#pragma require "XLMLockjaw.j2t" ///@MLLE-Generated
#pragma require "xlmlockjaw.j2l" ///@MLLE-Generated
#include "SEfirework-mlle.asc" ///@MLLE-Generated
#pragma require "SEfirework-mlle.asc" ///@MLLE-Generated
#include "Buzzsawer-mlle.asc" ///@MLLE-Generated
#pragma require "Buzzsawer-mlle.asc" ///@MLLE-Generated
#pragma require "ash.j2a"
#pragma require "Redeemer.j2a"
#pragma require "GBA-target.j2a"
#pragma require "FTURROCK.wav"
#pragma require "EXPSTD3.wav"
#pragma require "redeemer_flight.wav"
#pragma require "DamnUlt-Chelonia.png"
#pragma require "DamnUlt-Skull.png"
#pragma require "DamnUlt-Skeleton1.png"
#pragma require "DamnUlt-Skeleton2.png"
#pragma require "DamnUlt-Spiral1.png"
#pragma require "DamnUlt-Spiral2.png"
#pragma offer "2nd_pm-byproduct_remix.mp3"
#pragma require "chain-rattle1.wav"
#pragma require "chain-rattle2.wav"
#pragma require "adend54.wav"
#pragma require "adend55.wav"
#pragma require "godown-slam.wav"
#pragma require "wind21.wav"
#pragma require "lava31.wav"

bool sprited;
uint sprite;
int glowValue = 43;
int jawTimer = 0;
float jawYPos = 50 * 32;
int lockjaw_executorID = 33; // default value is outside max player range; person that activated the crusher trap and gets the roast(s)
int ambientChannel = 0;

array<uint> flagX = {13, 26, 73, 86, 128, 143, 173, 184};
array<uint> flagY = {2, 2, 0, 0, 1, 1, 14, 14};

bool isDestructibleItem(jjOBJ@ target) {
	return target.eventID == OBJECT::TNT || target.eventID == OBJECT::BOUNCERPOWERUP || target.eventID == OBJECT::GUN8POWERUP || target.eventID == OBJECT::GUN9POWERUP;
}

array<bool> controllingRedeemer(4, false), warning(32, false);
int xPos = 0, yPos = 0, cameraX = 0, cameraY = 0, redeemerAngle = 0, CTFArrowTimer = 0;
uint delay = 0;

int redeemerColor(jjPLAYER@ creator) {
	switch(creator.team) {
		case TEAM::BLUE: return 0;
		case TEAM::RED: return 1;
		case TEAM::GREEN: return 2;
		case TEAM::YELLOW: return 3;
	}
	return 0;
}

uint8 getTeamColor(TEAM::Color team) {
	switch (team) {
		case TEAM::BLUE: return 32;
		case TEAM::RED: return 24;
	}
	return 32;
}

const array<TEAM::Color> teams = {TEAM::BLUE, TEAM::RED, TEAM::GREEN, TEAM::YELLOW};
const float PI = 3.1415927f;

class Star {
	int xPos, yPos, color;
}
array<Star> Stars;

bool gameIsActive() {
	return jjGameState == GAME::STARTED || jjGameState == GAME::OVERTIME;
}

bool CTFMode() {
	return (jjGameMode == GAME::CTF && jjGameCustom == GAME::NOCUSTOM) || jjGameCustom == GAME::DCTF;
}

void onLevelLoad() {
	for (int i = 1; i < 255; i++) {
		if (jjObjectPresets[i].playerHandling == HANDLING::PICKUP && i != 72) {
			jjObjectPresets[i].isBlastable = false;
		}
	}
	
	jjAnimSets[ANIM::CUSTOM[9]].load(0, "ash.j2a");
	jjAnimSets [ANIM::SNOW].load();
	jjPAL snow = jjPAL();
	snow.load("Inferno1.j2t");
	jjObjectPresets[OBJECT::SNOW].behavior = SnowSpawner (palette: snow, yAcc: -0.015625);
	
	jjSampleLoad(SOUND::P2_POEP, "FTURROCK.wav");
	jjSampleLoad(SOUND::P2_PTOEI, "EXPSTD3.wav");
	jjSampleLoad(SOUND::P2_SPLOUT, "redeemer_flight.wav");
	jjSampleLoad(SOUND::ORANGE_BUBBELSL, "adend54.wav");
	jjSampleLoad(SOUND::ORANGE_BUBBELSR, "adend55.wav");
	jjSampleLoad(SOUND::INTRO_END, "godown-slam.wav");
	jjSampleLoad(SOUND::SCIENCE_PLOPKAOS, "lava31.wav");
	jjSampleLoad(SOUND::WIND_WIND2A, "wind21.wav");
	jjAnimSets[ANIM::SONCSHIP].load();
	jjAnimSets[ANIM::VINE].load();
	jjAnimSets[ANIM::FLAG].load();
	jjAnimSets[ANIM::CUSTOM[8]].load(0, "Redeemer.j2a");
	jjAnimations[jjAnimSets[ANIM::AMMO] + 59] = jjAnimations[jjAnimSets[ANIM::CUSTOM[8]] + 0];

///@Event 38=Redeemer Ammo (+1) |+|Ammo |Redee- |mer	
	jjObjectPresets[OBJECT::TNT].determineCurFrame();
	jjObjectPresets[OBJECT::TNT].behavior = Redeemer();
	jjObjectPresets[OBJECT::TNT].xSpeed = jjObjectPresets[OBJECT::BLASTERBULLET].xSpeed * 1.33;
	jjObjectPresets[OBJECT::TNT].counterEnd = 255;
	jjObjectPresets[OBJECT::TNT].killAnim = jjObjectPresets[OBJECT::SEEKERBULLET].killAnim;
	
	jjObjectPresets[OBJECT::TNTAMMO3].behavior = RedeemerPickup();
	jjObjectPresets[OBJECT::TNTAMMO3].determineCurFrame();
	jjObjectPresets[OBJECT::TNTAMMO3].scriptedCollisions = true;
	
	jjWeapons[WEAPON::TNT].maximum = 1;

///@Event 39=Roller Ammo (+3) |+|Ammo |Gun8 |Am+3	
//	jjWeapons[WEAPON::GUN8].comesFromGunCrates = true;
///@Event 40=Firework Ammo (+3) |+|Ammo |Gun9 |Am+3
//	jjWeapons[WEAPON::GUN9].comesFromGunCrates = true;
///@Event 220=Roller Power Up |+|PowerUp |Gun8 |Power
///@Event 221=Firework Power Up |+|PowerUp |Gun9 |Power
	
	generateColoredRedeemerSprites(jjAnimSets[ANIM::CUSTOM[10]], array<uint> = {32, 24, 16, 40});
	generateRedeemerPickupSprite(jjAnimSets[ANIM::CUSTOM[11]], array<uint> = {0});
	
	for (int i = 0; i < 256; i++) {
		Star newStar;
		newStar.xPos = jjRandom()%1200;
		newStar.yPos = jjRandom()%300;
		newStar.color = 72 + jjRandom()%7;
		Stars.insertLast(newStar);
	}

	///@Event 95=Trigger Target |+|Trigger |Trig |Targ |TriggerID:5|Set to:{On,Off}1|Direction:{Down,Right,Up,Left}2	
	jjAnimSets[ANIM::CUSTOM[14]].load(0, "GBA-target.j2a");
	jjOBJ@ triggerTargetPreset = jjObjectPresets[OBJECT::TRIGGERCRATE];
	triggerTargetPreset.behavior = triggerTarget();
	triggerTargetPreset.determineCurAnim(ANIM::CUSTOM[14], 0);
	triggerTargetPreset.playerHandling = HANDLING::SPECIAL;
	triggerTargetPreset.scriptedCollisions = true;
	triggerTargetPreset.isTarget = true;
	triggerTargetPreset.isFreezable = false;
	triggerTargetPreset.triggersTNT = true;
	triggerTargetPreset.deactivates = true;
	
///@Event 180=Decorative Vine|+|Scenery|Decor|Vine|Length:3|Foreground:c1|XOffset:3|YOffset:3
	jjSampleLoad(SOUND::INTRO_UP1, "chain-rattle1.wav");
	jjSampleLoad(SOUND::INTRO_UP2, "chain-rattle2.wav");
	const uint vineFrameID = jjAnimations[jjAnimSets[ANIM::CUSTOM[13]].allocate(array<uint> = {8})];
	for (uint i = 0; i < 8; ++i) {
		jjANIMFRAME@ vineFrame = jjAnimFrames[vineFrameID + i];
		jjPIXELMAP vineImage((187 + i) * 32 + 8, 82*32, 16, 52 + i*16, 4);
		for (uint x = 0; x < vineImage.width; ++x)
			for (uint y = 0; y < vineImage.height; ++y)
				if (vineImage[x,y] == 0)
					vineImage[x,y] = 128;
			vineImage.save(vineFrame);
			vineFrame.hotSpotX = -8;
	}
	jjOBJ@ preset = jjObjectPresets[180];
	preset.behavior = DecorativeVine;
	preset.curFrame = vineFrameID;
	preset.playerHandling = HANDLING::PARTICLE;

	jjAnimSets[ANIM::HELMUT].load();
	jjPIXELMAP chelonia("DamnUlt-Chelonia.png");
//	tree1.crop(0, 64, 960, 352); // shaving off top two tiles (you might not need to)
	jjANIMFRAME@ helmut1 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim].firstFrame];
	chelonia.save(helmut1);
	helmut1.hotSpotX = 0;
	helmut1.hotSpotY = 0;
	
	jjPIXELMAP skull("DamnUlt-Skull.png");
	jjANIMFRAME@ helmut2 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim].firstFrame + 1];
	skull.save(helmut2);
	helmut2.hotSpotX = 0;
	helmut2.hotSpotY = 0;	
	
	jjPIXELMAP skeleton1("DamnUlt-Skeleton1.png");
	jjANIMFRAME@ helmut3 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim].firstFrame + 2];
	skeleton1.save(helmut3);
	helmut3.hotSpotX = 0;
	helmut3.hotSpotY = 0;	
	
	jjPIXELMAP skeleton2("DamnUlt-Skeleton2.png");
	jjANIMFRAME@ helmut4 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim].firstFrame + 3];
	skeleton2.save(helmut4);
	helmut4.hotSpotX = 0;
	helmut4.hotSpotY = 0;	
	
	jjPIXELMAP spiral1("DamnUlt-Spiral1.png");
	jjANIMFRAME@ helmut5 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim + 1].firstFrame];
	spiral1.save(helmut5);
	helmut5.hotSpotX = 0;
	helmut5.hotSpotY = 0;	
	
	jjPIXELMAP spiral2("DamnUlt-Spiral2.png");
	jjANIMFRAME@ helmut6 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::HELMUT].firstAnim + 1].firstFrame + 1];
	spiral2.save(helmut6);
	helmut6.hotSpotX = 0;
	helmut6.hotSpotY = 0;	
	
	jjLAYER@ skycopy = jjLayers[5];
	skycopy.spriteMode = SPRITE::BLEND_NORMAL;
	skycopy.spriteParam = 128;
	skycopy.xSpeedModel = LAYERSPEEDMODEL::NORMAL;
	skycopy.ySpeedModel = LAYERSPEEDMODEL::NORMAL;
	
	jjLayers[8].xSpeed = 0.01;
	jjLayers[8].ySpeed = 0.01;
	jjLayers[8].xSpeedModel = LAYERSPEEDMODEL::NORMAL;
	jjLayers[8].ySpeedModel = LAYERSPEEDMODEL::NORMAL;
	
	jjPIXELMAP jaw(0, 90 * 32, 16 * 32, 10 * 32, 2);
	jjANIMFRAME@ ship1 = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::SONCSHIP].firstAnim + 2].firstFrame];
	jaw.save(ship1);
	ship1.hotSpotX = 0;
	ship1.hotSpotY = 0;
	
	jjLAYER@ noise = MLLE::GetLayer("Camera Noise");
	noise.spriteMode = SPRITE::BLEND_NORMAL;
	noise.spriteParam = 96;
	
	if (jjGameMode == GAME::CTF) {
		jjAddObject(OBJECT::CTFBASE, 8 * 32, 47 * 32 + 24, 0, CREATOR::LEVEL);
		jjOBJ@ redBase = jjObjects[jjAddObject(OBJECT::CTFBASE, 202 * 32, 47 * 32 + 36, 0, CREATOR::LEVEL)];
		redBase.var[1] = 1;
		redBase.var[2] = 1;
		for (int xTile = int(jjLayerWidth[4] / 2); --xTile >= 0;) {
			for (int yTile = jjLayerHeight[4]; --yTile >= 0;) {
				uint16 oldTile = jjTileGet(4, xTile, yTile);
				uint16 newTile = 0;
				bool tileChanged = false;
				
				switch (oldTile) {
					case 2507: newTile = 4024; tileChanged = true; break;
					case 2508: newTile = 4025; tileChanged = true; break;
					case 2517: newTile = 4026; tileChanged = true; break;
					case 2518: newTile = 4027; tileChanged = true; break;
					case 2527: newTile = 4028; tileChanged = true; break;
					case 2528: newTile = 4029; tileChanged = true; break;
				}
				if (tileChanged) jjTileSet(4, xTile, yTile, newTile);
			}
		}
	}
}

jjANIMSET@ coloredRedeemerSprite;
bool generateColoredRedeemerSprites(jjANIMSET@ anim, const array<uint> &in colors) {
	int length = colors.length();
	bool success = (@coloredRedeemerSprite = anim).allocate(array<uint>(length * 4, 8)) !is null;
	if (success) {
		uint srcSet = jjAnimSets[ANIM::CUSTOM[8]];
		for (int i = 0; i < length; i++) {
			uint color = colors[i];
			uint destAnimOffset = anim + i;
			for (int j = 0; j < 4; j++) {
				uint srcAnim = jjAnimations[srcSet + 0];
				uint destAnim = jjAnimations[destAnimOffset + j];
				for (int k = 0; k < 8; k++) {
					jjPIXELMAP image(jjAnimFrames[destAnim + k] = jjAnimFrames[srcAnim + k]);
					int width = image.width;
					int height = image.height;
					for (int l = 0; l < height; l++) {
						for (int m = 0; m < width; m++) {
							int pixel = image[m, l];
							if (pixel >= 88 && pixel < 96)
								image[m, l] = color + (pixel & 7);
						}
					}
					if (!image.save(jjAnimFrames[destAnim + k]))
						return false;
				}
			}
		}
	}
	return success;
}

jjANIMSET@ redeemerPickupSprite;
bool generateRedeemerPickupSprite(jjANIMSET@ anim, const array<uint> &in colors) {
	int length = colors.length();
	bool success = (@redeemerPickupSprite = anim).allocate(array<uint>(length, 8)) !is null;
	if (success) {
		uint srcSet = jjAnimSets[ANIM::SONCSHIP];
		for (int i = 0; i < length; i++) {
			uint color = colors[i];
			uint destAnimOffset = anim + i;
			uint srcAnim = jjAnimations[srcSet + 0];
			uint destAnim = jjAnimations[destAnimOffset + 0];
			for (int k = 0; k < 8; k++) {
				jjPIXELMAP image(jjAnimFrames[destAnim + k] = jjAnimFrames[srcAnim + k]);
				int width = image.width;
				int height = image.height;
				for (int l = 0; l < height; l++) {
					for (int m = 0; m < width; m++) {
						int pixel = image[m, l];
						if (pixel >= 40 && pixel < 48)
							image[m, l] = color;
					}
				}
			if (!image.save(jjAnimFrames[destAnim + k]))
				return false;
			}
		}
	}
	return success;
}

void onDrawLayer4(jjPLAYER@ player, jjCANVAS@ canvas) {
	for (uint i = 0; i < flagX.length; i++) canvas.drawSprite((flagX[i] * 32) + 10, (flagY[i] * 32) + 21, ANIM::FLAG, 8, (jjGameTicks/9) & 7, 0, jjGameMode == GAME::CTF && flagX[i] < 108 ? SPRITE::PALSHIFT : SPRITE::NORMAL, 8);
}

void onDrawLayer5(jjPLAYER@ player, jjCANVAS@ canvas) { // I'M DRAWING SPRITES ON THE TEXTURE LAYER AND NO ONE CAN STOP ME!!!!!
	canvas.drawSprite(288, 168, ANIM::HELMUT, 0, 1, -1, SPRITE::TINTED, 192);
	canvas.drawSprite(780, 304, ANIM::HELMUT, 0, 2, -1, SPRITE::TINTED, 192);
	canvas.drawSprite(512, 336, ANIM::HELMUT, 0, 3, -1, SPRITE::TINTED, 192);
}

void onDrawLayer7(jjPLAYER@ player, jjCANVAS@ canvas) {
	canvas.drawSprite(-160, -128, ANIM::HELMUT, 0, 0);
}

void onDrawLayer1(jjPLAYER@ player, jjCANVAS@ canvas) {
	canvas.drawSprite(556, 448, ANIM::HELMUT, 1, 1, 1, SPRITE::TINTED, 192);
}

void onDrawLayer2(jjPLAYER@ player, jjCANVAS@ canvas) {
	canvas.drawSprite(101 * 32, int(jawYPos), ANIM::SONCSHIP, 2, 0);
}

void onDrawLayer6(jjPLAYER@ play, jjCANVAS@ canvas) {
	if (!jjLowDetail && jjColorDepth == 16) {
		int end = int(play.cameraX) + jjSubscreenWidth + 2;
		int bottom = 24*32;
		for (int i = int(play.cameraX) & ~3 | 2; i < end; i += 4) {
			if (sprited)
				canvas.drawSpriteFromCurFrame(i, bottom, sprite, 0, SPRITE::ALPHAMAP, glowValue);
			else
				canvas.drawRectangle(i - 2, bottom, 4, -128, glowValue, SPRITE::BLEND_NORMAL, 64);
		}
	}
}

void onDrawLayer3(jjPLAYER@ play, jjCANVAS@ canvas) {
	canvas.drawSprite(640, 576, ANIM::HELMUT, 1, 0);
	canvas.drawSprite(1984, 576, ANIM::HELMUT, 1, 0, -1);
}

void onMain() {	
	jjPLAYER@ executor = jjPlayers[lockjaw_executorID];
	if (jjTriggers[0]) {
		jawTimer++;
		switch (jawTimer) {
		case 0:
			jjSample(100 * 32 + 16, 53 * 32, SOUND::ORANGE_BOEML);
			jjSample(117 * 32 + 16, 53 * 32, SOUND::ORANGE_BOEMR);
			jjSample(100 * 32 + 16, 45 * 32, SOUND::ORANGE_BOEML);
			jjSample(117 * 32 + 16, 45 * 32, SOUND::ORANGE_BOEMR);
			break;
		case 250:
			jawYPos = 41 * 32;
			jjSample(109 * 32, 45 * 32, SOUND::INTRO_END);
			break;
		case 370:
			lockjaw_executorID = 33;
			break;
		case 515:
			jjSample(100 * 32 + 16, 53 * 32, SOUND::ORANGE_BUBBELSL);
			jjSample(117 * 32 + 16, 53 * 32, SOUND::ORANGE_BUBBELSR);
			jjSample(100 * 32 + 16, 45 * 32, SOUND::ORANGE_BUBBELSL);
			jjSample(117 * 32 + 16, 45 * 32, SOUND::ORANGE_BUBBELSR);
			jawTimer = 0;
			jjTriggers[0] = false;
			break;
		}
		if (jawTimer >= 100 && jawTimer < 200) jawYPos += 0.5;
		else if (jawTimer >= 200 && jawTimer < 250) jawYPos -= 6.5;
		else if (jawTimer >= 370 && jawTimer < 515) jawYPos += 2;
		
		if (jjIsServer || jjGameConnection == GAME::LOCAL) {
			for (int8 i = 0; i < 32; i++) {
				jjPLAYER@ ip = jjPlayers[i];
				if (
						ip.isInGame 
				    && ip.xPos >= 101 * 32 
				    && ip.xPos <= 117 * 32 
				    && ip.yPos >= 43 * 32 
				    && ip.yPos <= 55 * 32
						) // trap cell area (rectangle)
							if (jawTimer == 250 && ip.health > 0 )
								ip.hurt(10, true, executor);
							if (ip.yPos > jawYPos + (4 * 32))
								ip.yPos = jawYPos + (4 * 32);
			}
		}
		else {
			for (int8 i = 0; i < 4; i++) {
				jjPLAYER@ ip = jjLocalPlayers[i];
				if (
						ip.isInGame 
				    && ip.xPos >= 101 * 32 
				    && ip.xPos <= 117 * 32 
				    && ip.yPos >= 43 * 32 
				    && ip.yPos <= 55 * 32
						) // trap cell area (rectangle)
							//if (jawTimer == 250 && ip.health > 0 )
								//ip.hurt(10, true, executor);
							if (ip.yPos > jawYPos + (4 * 32))
								ip.yPos = jawYPos + (4 * 32);
			}
		}
	}
	else jawYPos = 50 * 32;
}

void onLevelBegin() {
	jjAddObject(OBJECT:: SNOW, 0, 0);
	jjANIMSET@ anim;
	for (int i = 0; i < 256; i++) {
		jjANIMSET@ custom = jjAnimSets[ANIM::CUSTOM[i]];
		if (custom == 0) {
			@anim = @custom;
			break;
		}
	}
	if (sprited = anim !is null) {
			jjPIXELMAP image(4, 128);
			for (int i = 0; i < 128; i++) {
				int alpha = int(i*1.5);
				for (int j = 0; j < 4; j++) {
					image[j, i] = alpha;
				}
			}
			jjANIMFRAME@ frame = jjAnimFrames[sprite = jjAnimations[anim.allocate(array<uint>(1, 1))]];
			image.save(frame);
			frame.hotSpotX = -2;
			frame.hotSpotY = -128;
	}
	for (int i = 0; i < jjLocalPlayerCount; i++) {
		jjLocalPlayers[i].showText(0, 0);
	}
	jjOBJ@ crate = jjObjects[jjAddObject(OBJECT::GENERATOR, (32 * 32) + 16, (79 * 32) + 16)];
	crate.deactivates = false;
	crate.var[3] = 46; // Event parameter
	crate.var[2] = 700; // Delay secs parameter
}

class CustomSnow: jjBEHAVIORINTERFACE {
	private const jjPAL@ palette;
	private uint16 frameCount;
	
	CustomSnow (const jjPAL &in palette) {
		@this.palette = palette;
	}

	void onBehave (jjOBJ@ obj) {
		switch(obj.state) {
			case STATE::START: {
				obj.playerHandling = HANDLING::PARTICLE;
				obj.bulletHandling = HANDLING::IGNOREBULLET;
				frameCount = jjAnimations[obj.curAnim].frameCount;
				obj.frameID = jjRandom() % frameCount;
				obj.determineCurFrame();
				obj.state = hitSurface (obj) ? STATE::EXTRA : STATE::FLY;
			} break;
			case STATE::KILL:
				obj.delete();
				break;
			case STATE::DEACTIVATE:
				obj.deactivate();
				break;
			case STATE::HIT: {
				if (obj.frameID == int8(frameCount - 1)) {
					obj.state = STATE::KILL;
					return;
				}

				if (++obj.counter == obj.animSpeed) {
					obj.counter = 0;
					obj.frameID++;
					obj.determineCurFrame();
				}
			} break;
			default: {
				obj.xPos += obj.xSpeed;
				obj.xSpeed += ((jjRandom() % uint(obj.var[1])) / 65536.0) * jjCos(uint(jjGameTicks)) * obj.var[3];
				obj.yPos += obj.ySpeed;
				obj.ySpeed += ((jjRandom() % uint(obj.var[2])) / 65536.0) * obj.var[4];
				if ((obj.state == STATE:: EXTRA && --obj.counterEnd == 0) || (obj.state == STATE::FLY && hitSurface (obj))) {
					obj.state = STATE::HIT;
				}
			} break;
		}
	}
	
	void onDraw(jjOBJ@ obj) {
		if (obj.special < 0) {
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 0, SPRITE::NORMAL, 0, 4, 4, obj.var[0]);
		} else {
			jjSpriteModeSetMapping(obj.special, array<uint8>={}, palette);
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, 0, SPRITE::TRANSLUCENT, obj.special, 4, 4, obj.var[0]);
		}
	}

	private bool hitSurface (jjOBJ@ obj) {
		return jjMaskedPixel(int(obj.xPos), int(obj.yPos));
	}
}

class SnowSpawner: jjBEHAVIORINTERFACE {
	private float xSpeed;
	private float ySpeed;
	private int xAcc, xDir;
	private int yAcc, yDir;
	private jjPAL palette;
	private int mappingIndex = -1;
	private int animSpeed;
	private uint8 countdown;
	private ANIM::Set animSet;
	private uint8 animation;

	SnowSpawner (float xSpeed = -1, float ySpeed = 0, float xAcc = 0.015625, float yAcc = 0.015625, const jjPAL@ palette = null, int animSpeed = 10, uint8 countdown = 35, const ANIM::Set &in animSet = ANIM::CUSTOM[9], uint8 animation = 0) {
		this.xSpeed = xSpeed;
		this.ySpeed = ySpeed;
		this.xAcc = int(abs(xAcc) * 65536.0);
		this.xDir = xAcc < 0 ? -1 : 1;
		this.yAcc = int(abs(yAcc) * 65536.0);
		this.yDir = yAcc < 0 ? -1 : 1;
		int freeMapping = jjSpriteModeFirstFreeMapping();
		if (palette !is null && freeMapping >= 0) {
			jjSpriteModeSetMapping(freeMapping, array<uint8>={}, palette);
			this.mappingIndex = freeMapping;
			this.palette = palette;
		}
		this.animSpeed = animSpeed;
		this.countdown = countdown;
		this.animSet = animSet;
		this.animation = animation;
	}

	void onBehave (jjOBJ@ obj) {
		if (obj.state == STATE::START) {
			obj.curFrame = 0;
			obj.state = STATE::FLY;
			obj.playerHandling = HANDLING:: PARTICLE;
			obj.bulletHandling = HANDLING::IGNOREBULLET;
		}
		
		for (int i = 0; i < jjLocalPlayerCount; i++) {
			jjPLAYER@ player = jjLocalPlayers[i];
			uint cameraX = uint(player.cameraX) << 16;
			uint cameraY = uint(player.cameraY) << 16;
			uint playerxSpeed = uint(player.xSpeed) << 16;
			// g_Ai.c anyone?
			float xPos = (cameraX + playerxSpeed - 32*65536 + (jjRandom() & 32767) * 2 * (jjSubscreenWidth+64)) / 65536.0;
			float yPos = (cameraY - 32*65536 + (jjRandom() & 32767) * 2 * jjSubscreenHeight) / 65536.0;
			int objID = jjAddObject(OBJECT::SNOW, xPos, yPos, obj.objectID, CREATOR::OBJECT, null);
			if (objID > 0) {
				jjOBJ@ snow = jjObjects[objID];
				snow.counter = 0;
				snow.var[0] = player.playerID;
				snow.xSpeed = xSpeed;
				snow.ySpeed = ySpeed;
				snow.var[1] = xAcc;
				snow.var[2] = yAcc;
				snow.var[3] = xDir;
				snow.var[4] = yDir;
				snow.special = mappingIndex;
				snow.animSpeed = animSpeed;
				snow.counterEnd = countdown;
				snow.determineCurAnim(animSet, animation);
				snow.behavior = CustomSnow (palette);
			}
		}
	}
}

class Redeemer : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		//var[0] - angle
		//var[1] - angular speed
		//var[2] - has the redeemer exploded
		//special - value of jjGameTicks at the time the last packet was sent/received	
		
		int angle;
		float speed = jjObjectPresets[OBJECT::TNT].xSpeed;
		
		if (obj.creatorType != CREATOR::PLAYER)
			obj.delete();
		jjPLAYER@ creator = jjPlayers[obj.creatorID];
		
		switch (obj.state) {
			case STATE::START:
				obj.curAnim = jjObjectPresets[obj.eventID].curAnim;
				jjSamplePriority(SOUND::P2_POEP);
				obj.var[0] = int(atan2(-obj.ySpeed, obj.xSpeed) * (512.f * 0.318309886142228f));
				obj.ySpeed = speed * -jjSin(obj.var[0]);
				obj.xSpeed = speed * jjCos(obj.var[0]);
				obj.direction = obj.xSpeed < 0.f ? -1 : 1;
				obj.var[2] = 0;
				if (creator.isLocal) controllingRedeemer[creator.localPlayerID] = true;
				obj.state = STATE::FLY;
			break;
				
			case STATE::FLY:
				obj.deactivates = false; // useless in MP but needed for testing
				redeemerCamera(creator, obj);
				jjDrawRotatedSprite(obj.xPos, obj.yPos, (creator.isLocal || jjGameMode != GAME::CTF)? ANIM::CUSTOM[8] : ANIM::CUSTOM[10], (creator.isLocal || jjGameMode != GAME::CTF)? 1 : redeemerColor(creator), obj.curFrame, obj.var[0], 1, 1, SPRITE::NORMAL);
				if (creator.isLocal) {
					redeemerAngle = obj.var[0];
					if (!jjLowDetail && (obj.counter % 131 == 36 || obj.counter == 1) && controllingRedeemer[creator.localPlayerID]) jjSamplePriority(SOUND::P2_SPLOUT);
					float dx = creator.xPos - obj.xPos, dy = creator.yPos - obj.yPos;
						if (dx * dx + dy * dy < 420 * 420) warning[obj.creatorID] = true;
						else warning[obj.creatorID] = false;
					if (creator.health == 0) {
						creator.cameraUnfreeze(true);
						controllingRedeemer[creator.localPlayerID] = false;
					} else {
						if (controllingRedeemer[creator.localPlayerID]) creator.xSpeed = 0;
						if (obj.counter >= 21) {
							if (controllingRedeemer[creator.localPlayerID]) {
								if (creator.keyRight || (obj.direction < 0 ? creator.keyUp : creator.keyDown))
									obj.var[1] = -16;
								else if (creator.keyLeft || (obj.direction < 0 ? creator.keyDown : creator.keyUp))
									obj.var[1] = 16;
								else
									obj.var[1] = 0;
							}
							else obj.var[1] = 0;
						}
						if (obj.counter >= 127)
							obj.counter = 35;
						else if (obj.counter >= 35 && creator.keyFire && controllingRedeemer[creator.localPlayerID])
							obj.state = STATE::EXPLODE;
					}
					xPos = int(obj.xPos);
					yPos = int(obj.yPos);
				}
				if (obj.yPos >= jjLayerHeight[4]*32) obj.state = STATE::EXPLODE;
				obj.var[0] = obj.var[0] + obj.var[1];
				obj.ySpeed = speed * -jjSin(obj.var[0]);
				obj.xSpeed = speed * jjCos(obj.var[0]);
				if (obj.counter % 3 == 0 && !jjLowDetail) spawnFireTrail(obj);
			break;
			
			case STATE::EXPLODE:
				if (obj.var[2] == 0) {
					RedeemerExplosion temp;
					jjOBJ@ explosion = jjObjects[jjAddObject(OBJECT::BULLET, obj.xPos, obj.yPos, obj.creatorID, CREATOR::PLAYER, jjVOIDFUNCOBJ(temp.onBehave))];
					jjSamplePriority(SOUND::P2_PTOEI);
					obj.var[2] = 1;
					explosion.var[2] = 1;
				} else {
					jjDrawResizedSprite(obj.xPos, obj.yPos, ANIM::AMMO, 5, obj.curFrame + 5, 8, 8, SPRITE::NORMAL, 0, 2, 2);
				}
				creator.cameraUnfreeze(true);
				if (creator.isLocal) controllingRedeemer[creator.localPlayerID] = false;
			break;
		}
		
		int previousState = obj.state;
		obj.behave(BEHAVIOR::BULLET, false);
		if (!creator.isLocal) {
			if (obj.special + 128 > jjGameTicks)
				obj.state = previousState;
		} else if (obj.special + 4 <= jjGameTicks) {
			jjSTREAM packet;
            packet.push(uint8(packet_redeemer));
			packet.push(int8(obj.creatorID));
			packet.push(obj.state == STATE::EXPLODE);
			packet.push(obj.xPos);
			packet.push(obj.yPos);
			if (obj.state != STATE::EXPLODE) {
				packet.push(int16(obj.var[0]));
				packet.push(int16(obj.var[1]));
			}
			jjSendPacket(packet);
			obj.special = jjGameTicks;
		}
	}
}

void spawnFireTrail(jjOBJ@ obj) {
	jjOBJ@ trail = jjObjects[jjAddObject(OBJECT::EXPLOSION, int(obj.xPos - jjCos(obj.var[0])), int(obj.yPos - jjSin(obj.var[0])))];
	trail.determineCurAnim(ANIM::AMMO, 3);
	trail.lightType = LIGHT::POINT;
	trail.playerHandling = HANDLING::PARTICLE;
	trail.bulletHandling = HANDLING::IGNOREBULLET;
	trail.isBlastable = false;
}

void redeemerCamera(jjPLAYER@ creator, jjOBJ@ obj) {
	float dx = jjLayerWidth[4] - obj.xPos, dy = jjLayerHeight[4] - obj.yPos;
	
	if (jjLayerWidth[4] - dx <= 400) cameraX = 400;
	else cameraX = int(obj.xPos);
	
	if (jjLayerHeight[4] - dy <= 400) cameraY = 400;
	else cameraY = int(obj.yPos);
	
	if (controllingRedeemer[creator.localPlayerID]) {
		creator.cameraFreeze(int(obj.xPos) >= 8528? 8528 : cameraX, int(obj.yPos) >= 4080? 4080 : cameraY, true, true);
	
		for (int i = 0; i < 32; i++) {
			jjPLAYER@ play = jjPlayers[i];
			float reticleScale = jjSin(jjGameTicks*5);
			float reticleColor = jjSin(jjGameTicks*10)*2;
			float pdx = play.xPos - obj.xPos, pdy = play.yPos - obj.yPos;
			if ((pdx * pdx + pdy * pdy < 320 * 320) && (jjGameMode != GAME::CTF || jjFriendlyFire || play.team != creator.team) && !play.isLocal)
				jjDrawResizedSprite(play.xPos, play.yPos, ANIM::SONCSHIP, 7, 0, reticleScale + 2, reticleScale + 2, SPRITE::SINGLECOLOR, int(reticleColor + 18), 1);
							
		}
	}
}

class RedeemerPickup : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::PICKUP, false);
		obj.direction = obj.xPos > 4464? -1:1;
		if (obj.isActive) jjDrawSprite(int(obj.xPos) - (6 * obj.direction), int(obj.yPos) + jjSin(jjGameTicks*10)*6, ANIM::AMMO, 59, obj.curFrame);
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) {
		if (play.ammo[WEAPON::TNT] < jjWeapons[WEAPON::TNT].maximum) {
			if (play.isLocal) {
				play.ammo[WEAPON::TNT] = play.ammo[WEAPON::TNT] + 1;
				if (jjAutoWeaponChange) play.currWeapon = WEAPON::TNT;
				jjSample(obj.xPos, obj.yPos, SOUND::COMMON_LOADSPAZ, 0, 0);
			}
			obj.behavior = BEHAVIOR::EXPLOSION2;
			obj.scriptedCollisions = false;
			obj.frameID = 0;
		}
		return true;
	}
}

class RedeemerExplosion : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		//var[0] - obj.counter
		//var[2] - has the redeemer exploded
		//var[4] - blast radius
		//var[5] - damage
		//var[8] - has the player been hit by the explosion
	
		obj.playerHandling = HANDLING::PARTICLE;
		obj.bulletHandling = HANDLING::IGNOREBULLET;
		
		obj.lightType = obj.var[2] == 1? LIGHT::RING2 : LIGHT::NONE;
		
		jjPLAYER@ creator = jjPlayers[obj.creatorID];
		jjPLAYER@ play = jjLocalPlayers[0];
		
		if (obj.var[2] == 1) {
			obj.var[0] = obj.var[0] + 1;
			obj.light += 2;
			obj.var[4] = obj.light * 5;
			
			if (obj.var[4] >= 460) obj.var[5] = 1;
			else if (obj.var[4] >= 360 && obj.var[4] < 460) obj.var[5] = 2;
			else if (obj.var[4] < 360) obj.var[5] = 3;
			
			for (int i = 1; i < jjObjectCount; i++) {
				jjOBJ@ target = jjObjects[i];
				float dx = target.xPos - obj.xPos, dy = target.yPos - obj.yPos;
				if (isDestructibleItem(target) && target.var[4] == 0) {
					if (dx * dx + dy * dy < obj.var[4] * obj.var[4]) {
						if (target.eventID != OBJECT::TNT) creator.objectHit(target, -1, HANDLING::SPECIAL);
						else target.state = STATE::EXPLODE;
						target.var[4] = 1;
					}
					else target.var[4] = 0;
				}
				else if (target.behavior == BEHAVIOR::PICKUP && target.state == STATE::FLOAT) {
					if (dx * dx + dy * dy < obj.var[4] * obj.var[4])
						target.state = STATE::FLOATFALL;
				}
			}
			
			float pdx = play.xPos - obj.xPos, pdy = play.yPos - obj.yPos;
			if (pdx * pdx + pdy * pdy < 1600 * 1600) {
				uint random = jjRandom();
				int magnitude = (2 << (random & 3)) - 1;
				int halfMagnitude = magnitude >> 1;
				
				if ((jjGameTicks & 1 == 0 && !jjTriggers[19]) || (jjGameTicks & 1 == 0 && jjTriggers[19] && jjMaskedHLine(int(play.xPos) - 12, 24, int(play.yPos) + 21)) && !controllingRedeemer[creator.localPlayerID])
					play.cameraFreeze(play.cameraX + (random & magnitude) - halfMagnitude, play.cameraY + (random >> 8 & magnitude) - halfMagnitude, false, true);
				else
					play.cameraUnfreeze();
			}
		}
		
		if (jjIsServer && gameIsActive()) {
			for (int i = 0; i < 32; i++) {
				jjPLAYER@ player = jjPlayers[i];
				if (
					player.isActive && player.isInGame && player.health > 0 &&
						(jjGameMode != GAME::CTF || jjFriendlyFire || player.team != creator.team || player is creator)
				) {
					float dx = player.xPos - obj.xPos, dy = player.yPos - obj.yPos;
					if (dx * dx + dy * dy < obj.var[4] * obj.var[4]) {
						if (obj.var[8] & 1 << i == 0) {
							player.hurt(obj.var[5], true, creator);
							obj.var[8] = obj.var[8] | 1 << i;
						}
					}
				}
			}
		}
		
		if (obj.var[0] == 1) {
			jjSample(obj.xPos, obj.yPos, SOUND::BILSBOSS_FIRE, 0, 0);
			for (int i = -8; i <= 8; i+=8) {
				for (int j = -8; j <= 8; j+=8) {
					if (i != 0 || j != 0) {
						Fireworks temp;
						int id = jjAddObject(OBJECT::ELECTROBULLET, obj.xPos, obj.yPos, obj.creatorID, CREATOR::PLAYER, jjVOIDFUNCOBJ(temp.onBehave));
						if (id != 0) {
							jjOBJ@ flares = jjObjects[id];
							flares.xSpeed = j*2;
							flares.ySpeed = i*2;
						}
					}
				}
			}
		}
		else if (obj.var[0] == 70) {
			obj.var[0] = 0;
			obj.var[2] = 0;
			obj.var[4] = 0;
			if (!controllingRedeemer[creator.localPlayerID]) play.cameraUnfreeze();
			obj.delete();
		}
	}
}

class Fireworks : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) {
		obj.behave(BEHAVIOR::ELECTROBULLET, false);
		obj.counterEnd = 210;
		if (obj.ySpeed < 10 && obj.xSpeed != 0) obj.ySpeed += 0.5;
		obj.playerHandling = HANDLING::PARTICLE;
		obj.bulletHandling = HANDLING::IGNOREBULLET;
		if (obj.state == STATE::FLY) obj.particlePixelExplosion(1);
	}
}

void onReceive(jjSTREAM &in packet, int clientID) {
    uint8 type;
    packet.pop(type);

    if (type == packet_trigger_lockjaw) {
        lockjaw_packet_recv(packet);
    } else if (type == packet_redeemer) {
        int8 playerID;
        bool explosion;
        float xPos, yPos;
        int16 angle, angleSpeed;
        jjSTREAM packetBackup;
        if (jjIsServer)
            packetBackup = packet;
        if (packet.pop(playerID) && playerID >= 0 && playerID < 32 &&
            packet.pop(explosion) && packet.pop(xPos) && packet.pop(yPos) &&
            (explosion || packet.pop(angle) && packet.pop(angleSpeed))
        ) {
            const jjPLAYER@ player = jjPlayers[playerID];
            if (!jjIsServer || player.isActive && player.isInGame && player.clientID == clientID ) {
                jjOBJ@ redeemer;
                for (int i = 0; i < jjObjectCount; i++) {
                    jjOBJ@ obj = jjObjects[i];
                    if (obj.isActive && obj.eventID == OBJECT::TNT && obj.creatorType == CREATOR::PLAYER && obj.creatorID == uint(playerID)) {
                        @redeemer = obj;
                        break;
                    }
                }
                if (redeemer is null && jjGameTicks < 140) {
                    int id = jjAddObject(OBJECT::TNT, xPos, yPos, playerID, CREATOR::PLAYER);
                    if (id > 0)
                        @redeemer = jjObjects[id];
                }
                if (redeemer !is null) {
                    if (jjIsServer)
                        jjSendPacket(packetBackup, -clientID);
                    if (explosion)
                        redeemer.state = STATE::EXPLODE;
                    redeemer.xPos = xPos;
                    redeemer.yPos = yPos;
                    redeemer.var[0] = angle;
                    redeemer.var[1] = angleSpeed;
                    redeemer.special = jjGameTicks;
                }
            }
        }
    } else {
        jjConsole("Got unknown packet with type: " + type);
    }
}

void onPlayer(jjPLAYER@ play) {
	if (controllingRedeemer[play.localPlayerID] && CTFMode()) {
		if (CTFArrowTimer > jjGameTicks + 280) CTFArrowTimer = jjGameTicks;
		if (CTFArrowTimer < jjGameTicks) {
			if (CTFArrowTimer + 64 >= jjGameTicks) {
				for (int i = 0; i < 32; i++) {
					jjPLAYER@ target = jjPlayers[i];
					if (target.flag != 0 && target.team != jjLocalPlayers[0].team) {
						int angle = int(atan2(target.yPos - yPos, target.xPos - xPos) * (512 / PI));
						const float scale = 64.f / (112.f - jjSin((jjGameTicks - CTFArrowTimer) << 3) * 64.f);
						jjDrawRotatedSprite(xPos + 32 * jjCos(angle), yPos + 32 * jjSin(angle), ANIM::FLAG, 0, 0, 970 - angle, scale, scale, SPRITE::PALSHIFT, getTeamColor(target.team) - 32, 1);
					}
				}
			} else {
				CTFArrowTimer = jjGameTicks + 210;
			}
		}
	}
	if (controllingRedeemer[play.localPlayerID]) play.currWeapon = WEAPON::BLASTER;
	if (play.yPos < 60 * 32) play.lighting = 60 + uint(play.yPos / 48);
	else play.lighting = 100;
	jjLAYER@ camLens = MLLE::GetLayer("Camera Lens");
	jjLAYER@ noise = MLLE::GetLayer("Camera Noise");
	if (jjLocalPlayerCount > 1 || jjLowDetail) camLens.hasTiles = noise.hasTiles = false; // no way around this for now I'm afraid
	else camLens.hasTiles = noise.hasTiles = controllingRedeemer[play.localPlayerID];
	
	if (play.yPos < 28 * 32) jjSampleLooped(play.xPos, 12 * 32, SOUND::WIND_WIND2A, ambientChannel);
	else if (play.yPos > 72 * 32) jjSampleLooped(play.xPos, 81 * 32, SOUND::SCIENCE_PLOPKAOS, ambientChannel);
}

bool onDrawAmmo(jjPLAYER@ play, jjCANVAS@ canvas) {
	bool smallScreen = jjSubscreenWidth <= 400;
	if (controllingRedeemer[play.localPlayerID]) {
		//canvas.drawResizedSprite(int(jjSubscreenWidth / 2), int(jjSubscreenHeight / 2), ANIM::CUSTOM[8], 2, 0, jjSubscreenWidth / 800, jjSubscreenHeight / 600, SPRITE::NORMAL);
		//it appears that currently drawResizedSprite can't handle such big sprites so this line is cut for now (and the current image might cover too much anyway)
		canvas.drawString(
			int(jjSubscreenWidth / 2) - int(jjGetStringWidth("Redeemer!", STRING::MEDIUM, STRING::NORMAL) / 2),
			int(jjSubscreenHeight / 4),
			"Redeemer!",
			STRING::MEDIUM,
			STRING::NORMAL
		);
		canvas.drawString(
			int(jjSubscreenWidth / 2) - int(jjGetStringWidth("Redeemer pos 000,000", STRING::SMALL, STRING::NORMAL) / 2),
			int(jjSubscreenHeight / 4) + 28,
			"Redeemer pos " + int(xPos / 32) + "," + int(yPos / 32),
			STRING::SMALL,
			STRING::NORMAL
		);
		canvas.drawString(
			int(jjSubscreenWidth / 2) - int(jjGetStringWidth("Use the movement keys to rotate the missile", STRING::SMALL, STRING::NORMAL) / 2),
			int(jjSubscreenHeight / 4) + 44,
			"|Use the |||movement keys |||||to rotate the missile",
			STRING::SMALL,
			STRING::NORMAL
		);
		canvas.drawString(
			int(jjSubscreenWidth / 2) - int(jjGetStringWidth("Press FIRE to detonate the missile in mid-air", STRING::SMALL, STRING::NORMAL) / 2),
			int(jjSubscreenHeight / 4) + 60,
			"|Press |||FIRE |||||to detonate the missile in mid-air",
			STRING::SMALL,
			STRING::NORMAL
		);
		canvas.drawRectangle(
			int(jjSubscreenWidth / 16),
			int(jjSubscreenHeight / 1.7),
			140,
			140,
			87,
			SPRITE::TRANSLUCENT
		);
		canvas.drawString(
			int(jjSubscreenWidth / 8.7),
			int(jjSubscreenHeight / 1.2),
			"Radar",
			STRING::SMALL,
			STRING::PALSHIFT,
			16
		);
		canvas.drawRotatedSprite(
			int(jjSubscreenWidth / 16) + 70, 
			int(jjSubscreenHeight / 1.7) + 70, 
			ANIM::CUSTOM[11],
			0,
			jjGameTicks >> 2,
			redeemerAngle, 
			1, 
			1, 
			SPRITE::SINGLEHUE, 
			80
		);
		
		for (int i = 0; i < 32; i++) {
			jjPLAYER@ player = jjPlayers[i];
			
			uint8 teamColor;
			switch (player.team) {
				case TEAM::BLUE: teamColor = 34; break;
				case TEAM::RED: teamColor = 24; break;
				case TEAM::GREEN: teamColor = 18; break;
				case TEAM::YELLOW: teamColor = 40; break;
				default: teamColor = 24; break;
			}
			
			int radarOffsetX = int(xPos - player.xPos) / 35;
			int radarOffsetY = int(yPos - player.yPos) / 35;
			
			if (radarOffsetX < 70 && radarOffsetX > -63 && radarOffsetY < 70 && radarOffsetY > -63 && player.xPos > 0 && player.yPos > 0 && !play.isSpectating && !play.isOut && !play.isConnecting) {
				if (player.flag != 0 && !player.isLocal) {
					canvas.drawResizedSprite(
						(int(jjSubscreenWidth / 16) + 68 - radarOffsetX),
						(int(jjSubscreenHeight / 1.7) + 68 - radarOffsetY),
						ANIM::FLAG,
						3,
						jjGameTicks >> 2,
						radarOffsetX > 0? 0.4:-0.4,
						0.4,
						SPRITE::SINGLECOLOR,
						player.team == TEAM::BLUE? 24 : 34
					);
					canvas.drawRectangle(
						(int(jjSubscreenWidth / 16) + 70 - radarOffsetX),
						(int(jjSubscreenHeight / 1.7) + 70 - radarOffsetY),
						6,
						6,
						teamColor,
						SPRITE::NORMAL
					);
				}
				else if (player.flag == 0) {
					canvas.drawRectangle(
						(int(jjSubscreenWidth / 16) + 70 - radarOffsetX),
						(int(jjSubscreenHeight / 1.7) + 70 - radarOffsetY),
						player.isLocal? 8:6,
						player.isLocal? 8:6,
						player.isLocal? 64 : jjGameMode == GAME::CTF? teamColor : 24,
						SPRITE::NORMAL
					);
				}
			}
			
			for (int j = 1; j < jjObjectCount; j++) {
				if (jjObjects[j].eventID == OBJECT::TNT) {
					
					uint8 redeemerColor;
					switch (jjPlayers[jjObjects[j].creatorID].team) {
						case TEAM::BLUE: redeemerColor = 34; break;
						case TEAM::RED: redeemerColor = 24; break;
						case TEAM::GREEN: redeemerColor = 18; break;
						case TEAM::YELLOW: redeemerColor = 40; break;
						default: redeemerColor = 88; break;
					}
					
					int radarMissileOffsetX = int(xPos - jjObjects[j].xPos) / 35;
					int radarMissileOffsetY = int(yPos - jjObjects[j].yPos) / 35;
					
					if (!jjPlayers[jjObjects[j].creatorID].isLocal && radarMissileOffsetX < 70 && radarMissileOffsetX > -63 && radarMissileOffsetY < 70 && radarMissileOffsetY > -63) {
						canvas.drawRotatedSprite(
							(int(jjSubscreenWidth / 16) + 70 - radarMissileOffsetX),
							(int(jjSubscreenHeight / 1.7) + 70 - radarMissileOffsetY),
							ANIM::CUSTOM[11],
							0,
							jjGameTicks >> 2,
							jjObjects[j].var[0],
							0.5,
							0.5,
							SPRITE::SINGLECOLOR,
							jjGameMode == GAME::CTF? redeemerColor : 90
						);
					}
				}
			}
		}
		
		if (warning[play.playerID]) {
			canvas.drawString(
				int(jjSubscreenWidth / 2) - int(jjGetStringWidth("WARNING: YOU ARE IN RANGE!", STRING::SMALL, STRING::NORMAL) / 2),
				int(jjSubscreenHeight / 4) + 76,
				"||WARNING: YOU ARE IN RANGE!",
				STRING::SMALL,
				STRING::NORMAL
			);
		}
		
		if (!jjLowDetail && jjGameTicks % 140 >= 7) {
			canvas.drawString(
				int(jjSubscreenWidth / 1.2),
				int(jjSubscreenHeight / 1.1),
				"REC",
				STRING::MEDIUM,
				STRING::NORMAL
			);
			canvas.drawSprite(
				int(jjSubscreenWidth / 1.2) - 14,
				int(jjSubscreenHeight / 1.1) + 1,
				ANIM::VINE,
				0,
				0,
				0,
				SPRITE::NORMAL
			);
		}
		return true;
	}
	else if (!controllingRedeemer[play.localPlayerID] && play.currWeapon == WEAPON::TNT) {
		canvas.drawString(
			smallScreen ? jjSubscreenWidth - 40 : jjSubscreenWidth - 80,
			smallScreen ? jjSubscreenHeight - 9 : jjSubscreenHeight - 14,
			"x" + play.ammo[WEAPON::TNT],
			smallScreen ? STRING::SMALL : STRING::MEDIUM,
			STRING::NORMAL
		);
		canvas.drawSprite(
			smallScreen ? jjSubscreenWidth - 64 : jjSubscreenWidth - 104,
			smallScreen ? jjSubscreenHeight - 9 : jjSubscreenHeight - 14,
			ANIM::AMMO,
			59,
			jjGameTicks >> 2
		);
		return true;
	}
	else return MLLE::WeaponHook.drawAmmo(play, canvas);
}

enum packet_type {
    packet_redeemer,
    packet_trigger_lockjaw,
};

void lockjaw_packet_send(int trigger, bool trigger_val, int executorID) {
    jjSTREAM stream;
    //set packet type
    stream.push(uint8(packet_trigger_lockjaw));
    
    stream.push(trigger);
    stream.push(trigger_val);
    stream.push(executorID);
    
    jjSendPacket(stream);
}

void lockjaw_packet_recv(jjSTREAM &in stream) {
    int trigger;
    bool trigger_val;
    int executorID;
    stream.pop(trigger);
    stream.pop(trigger_val);
    stream.pop(executorID);

    if (jjIsServer) lockjaw_executorID = executorID;
	jjTriggers[trigger] = trigger_val;
}

class triggerTarget : jjBEHAVIORINTERFACE {
bool checkForSwitch;
	void onBehave(jjOBJ@ obj) {
		switch (obj.state) {
			case STATE::START:
				if (jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 5,1) == 0) checkForSwitch = true;
				else checkForSwitch = false;
				obj.var[3] = jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0,5); // trigger ID
				if (jjTriggers[obj.var[3]] != checkForSwitch) obj.state = STATE::IDLE;
				else obj.state = STATE::SLEEP;
				break;
			case STATE::IDLE:
				obj.var[1] = 0; // animation counter
				obj.var[2] = 3; // number of spins to do
				break;
			case STATE::FADEOUT:
				obj.var[1] = obj.var[1] + 1;
				if (obj.var[2] > 1) {
					if (obj.var[1] == jjAnimations[obj.curAnim].frameCount * obj.age) {
						obj.var[1] = 0;
						obj.var[2] = obj.var[2] - 1;
					}
				}
				else if (obj.var[1] >= int((jjAnimations[obj.curAnim].frameCount * obj.age) / 1.75)) obj.state = STATE::SLEEP;
				break;
			case STATE::SLEEP:
				obj.var[1] = int((jjAnimations[obj.curAnim].frameCount * obj.age) / 1.75);
				if (jjTriggers[obj.var[3]] != checkForSwitch) {
					obj.var[1] = 34;
					obj.state = STATE::FADEIN;
				}
				break;
			case STATE::FADEIN:
				if (obj.var[1] == 0) obj.state = STATE::IDLE;
				else obj.var[1] = obj.var[1] - 1;
				break;
			case STATE::DEACTIVATE:
				obj.deactivate();
				break;
		}
	}
	void onDraw(jjOBJ@ obj) {
		obj.age = obj.var[2] * 4; // rotation speed
		if (obj.state == STATE::SLEEP) obj.frameID = 4;
		else obj.frameID = int8(obj.var[1] / 7);
		obj.determineCurFrame();
		if (obj.justHit > 0) jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, (jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 6,2) * 256), 1, 1, SPRITE::SINGLECOLOR, 15);
		else jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, (jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 6,2) * 256));
					
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (obj.state == STATE::IDLE && bullet !is null) {
			obj.justHit = 5;
			bullet.state = STATE::EXPLODE;
			jjSample(obj.xPos, obj.yPos, SOUND::PINBALL_BELL);
			jjTriggers[obj.var[3]] = checkForSwitch;
			lockjaw_executorID = player.playerID;
			obj.state = STATE::FADEOUT;
      lockjaw_packet_send(obj.var[3], checkForSwitch, lockjaw_executorID);
			return true;
        }
		else return false;
	}
}

void DecorativeVine(jjOBJ@ obj) {
	if (obj.state == STATE::START) {
		const uint eventX = uint(obj.xPos) >> 5, eventY = uint(obj.yPos) >> 5;
		const int length = jjParameterGet(eventX,eventY, 0, 3);
		obj.age = jjRandom();
		obj.curFrame += length;
		obj.var[0] = 50 + length*16;
		obj.var[1] = 4 - jjParameterGet(eventX,eventY, 3, 1);
		obj.xPos += jjParameterGet(eventX,eventY, 4, 3) << 2;
		obj.yPos += jjParameterGet(eventX,eventY, 8, 3) << 2;
		obj.state = STATE::IDLE;
	} else if (obj.state == STATE::DEACTIVATE) {
		obj.deactivate();
	} else {
		if (!jjLowDetail)
			jjDrawSwingingVineSpriteFromCurFrame(int(obj.xPos), int(obj.yPos), obj.curFrame, obj.var[0], int(jjSin(obj.age += 6) * 0x4000), SPRITE::NORMAL,0, obj.var[1]);
			if ((jjSin(obj.age) * 0x4000) % 12000 == 0) playRandomRattle(obj.xPos, obj.yPos);
	}
}

void playRandomRattle(float xPos, float yPos) {
	switch(jjRandom()%2) {
	  	case 0: jjSample(xPos, yPos, SOUND::INTRO_UP1, 63, 42000 + jjRandom()%4000); break;
	  	case 1: jjSample(xPos, yPos, SOUND::INTRO_UP2, 63, 42000 + jjRandom()%4000); break;
	}
}