Downloads containing ab21btl11.j2as

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Anniversary Bash 21 Levels Jazz2Online Multiple N/A Download file

File preview

#include "MLLE-Include-1.4.asc"
const bool MLLESetupSuccessful = MLLE::Setup();
#pragma require "ab21btl11.j2l"
#pragma require "Castle2N.j2t"
#pragma require "ab21btl11-MLLE-Data-1.j2l"
#pragma require "Sirius1.j2t"
#pragma require "SwampsD.j2t"
#pragma require "LaserBlaster.j2a"
#pragma require "f_laser1.wav"
#pragma require "f_laser2.wav"
#pragma require "f_laser3.wav"
#pragma require "f_laser4.wav"
#pragma require "bow_fire_arrow.wav"
#pragma require "floorslide.wav"
#pragma require "floorslideclose.wav"
#pragma require "expmine.wav"
#pragma require "meteor.j2a"
#pragma require "ArrowGun.j2a"
#pragma require "SEroller.asc"
#include "SEroller.asc"
enum PacketType {
	trapPacket,
	weaponPacket
}
enum ProjectileDrawingMethod {
	simple,
	angled,
	rotating
}
const int quitTrapSelectionKey = 0x08;
se::DefaultWeaponHook weaponHook(weaponPacket);
array<PlayerData> playerData(jjLocalPlayerCount);
array<Trap> traps;
interface TrapBehavior {
	bool isActive() const;
	void activate(jjPLAYER@ player);
	void process();
}
class PlayerData {
	int trapSelection = -1;
	int trapPrice = 0;
	bool prevKeyFire = false;
	bool prevKeyLeft = false;
	bool prevKeyRight = false;
}
class Trap {
	string name;
	string description;
	float x;
	float y;
	TrapBehavior@ behavior;
}
mixin class TimedTrapBehavior : TrapBehavior {
	bool isActive() const override {
		return timer != 0;
	}
	private int timer = 0;
}
class SpikeTrap : TimedTrapBehavior {
	SpikeTrap(int xTile, int yTile, int width, int xorTile) {
		x1 = xTile;
		x2 = xTile + width;
		y = yTile;
		mask = xorTile;
	}
	void activate(jjPLAYER@ player) override {
		auto@ data = playerData[player.localPlayerID];
		if (!isActive()) {
			switchTiles();
			if (data.trapSelection >= 0) {
				jjSample(73*32, 57*32, SOUND::COMMON_COLLAPS, 0, 0);
			} else {
				jjSample(37*32, 80*32, SOUND::COMMON_COLLAPS, 0, 0);
			}
		}
		@activator = player;
		timer = 210;
		if (!player.isLocal) jjAlert(""+player.nameUnformatted + " activated the |||Crystal Spike Trap", true, STRING::SMALL);
		if (jjIsServer && player.isLocal) jjAlert(""+player.nameUnformatted + " activated the |||Crystal Spike Trap", false, STRING::SMALL);
	}
	void process() override {
		for (int i = 0; i < jjLocalPlayerCount; i++) {
			auto@ player = jjLocalPlayers[i];
			auto@ data = playerData[player.localPlayerID];
			
			if (isActive()) {
				timer--;
				if (!isActive()) {
					switchTiles();
					if (data.trapSelection >= 0) {
						jjSample(73*32, 57*32, SOUND::CATERPIL_RIDOE, 0, 0);
					} else {
						jjSample(37*32, 80*32, SOUND::CATERPIL_RIDOE, 0, 0);
					}
				}
			}
			if (player.isInGame && player.xPos >= x1 << 5 && player.xPos < x2 << 5 && player.yPos > y << 5)
				killLocalPlayerWithTrap(player, activator);
		}
	}
	protected void switchTiles() const {
		for (int i = x1; i < x2; i++) {
			jjTileSet(4, i, y, jjTileGet(4, i, y) ^ mask);
		}
	}
	private int x1;
	private int x2;
	private int y;
	private int mask;
	private jjPLAYER@ activator;
}
class FireTrap : TimedTrapBehavior {
	FireTrap(int xTile, int yTile, int height) {
		x = xTile << 5;
		y = yTile << 5 | 16;
		count = height;
	}
	void activate(jjPLAYER@ player) override {
		@activator = player;
		timer = 210;
		if (!player.isLocal) jjAlert(""+player.nameUnformatted + " activated the |||Fire Trap", true, STRING::SMALL);
		if (jjIsServer && player.isLocal) jjAlert(""+player.nameUnformatted + " activated the |||Fire Trap", false, STRING::SMALL);
	}
	void process() override {
		if (isActive()) {
			timer--;
			for (int i = 0; i < 4; i++) {
				sampleSource = jjSampleLooped(x, y + (i*32), SOUND::COMMON_FLAMER, sampleSource, 0, 0);
				if (activator.isLocal) sampleActivator = jjSampleLooped(75*32, 58*32, SOUND::COMMON_FLAMER, sampleActivator, 0, 0);
			}
			if (timer & 1 == 0) {
				int id = jjAddObject(OBJECT::BULLET, x, y + ((timer >> 1) % count << 5), activator.playerID, CREATOR::PLAYER);
				if (id > 0) {
					auto@ obj = jjObjects[id];
					obj.age = 4;
					obj.behavior = Projectile(simple);
					obj.counterEnd = 36;
					obj.determineCurAnim(ANIM::AMMO, 55);
					obj.determineCurFrame();
					obj.playerHandling = HANDLING::SPECIAL;
					obj.scriptedCollisions = true;
					obj.xSpeed = 9.f;
					
				}
			}
		}
	}
	private float x;
	private float y;
	private int count;
	private int sampleSource, sampleActivator;
	private jjPLAYER@ activator;
}
class VolcanoTrap : TimedTrapBehavior {
	VolcanoTrap(int xTile, int yTile) {
		x = xTile << 5;
		y = yTile << 5;
	}
	void activate(jjPLAYER@ player) override {
		@activator = player;
		timer = 49;
		if (!player.isLocal) jjAlert(""+player.nameUnformatted + " activated the |||Volcano Trap", true, STRING::SMALL);
		if (jjIsServer && player.isLocal) jjAlert(""+player.nameUnformatted + " activated the |||Volcano Trap", false, STRING::SMALL);
	}
	void process() override {
		if (isActive()) {
			timer--;
			if (timer & 7 == 0) {
				for (int i = timer != 0 ? -1 : 0; i <= 1; i += 2) {
					int id = jjAddObject(OBJECT::BULLET, x, y, activator.playerID, CREATOR::PLAYER);
					if (id > 0) {
						auto@ obj = jjObjects[id];
						jjSample(x, y - 64, jjRandom()%2 > 0? SOUND::AMMO_FIREGUN1A : SOUND::AMMO_FIREGUN2A, 0, 0);
						if (activator.isLocal) jjSample(75*32, 58*32, jjRandom()%2 > 0? SOUND::AMMO_FIREGUN1A : SOUND::AMMO_FIREGUN2A, 0, 0);
						obj.age = 7;
						obj.behavior = Projectile(angled);
						obj.determineCurAnim(ANIM::BILSBOSS, 3);
						obj.determineCurFrame();
						obj.killAnim = jjAnimSets[ANIM::AMMO] + 5;
						obj.playerHandling = HANDLING::SPECIAL;
						obj.scriptedCollisions = true;
						obj.xSpeed = (timer >> 3) * i * 0.55f;
						obj.ySpeed = -13.f;
						obj.yAcc = 0.25f;
						obj.var[1] = 1;
						if (activator.isLocal) obj.var[2] = 1;
					}
				}
			}
		}
	}
	private float x;
	private float y;
	private int sampleSource, sampleActivator;
	private jjPLAYER@ activator;
}
class DartTrap : TimedTrapBehavior {
	DartTrap(int xTile, int yTile) {
		x = xTile << 5;
		y = yTile << 5;
	}
	void activate(jjPLAYER@ player) override {
		@activator = player;
		timer = 1;
		if (!player.isLocal) jjAlert(""+player.nameUnformatted + " activated the |||Dart Trap", true, STRING::SMALL);
		if (jjIsServer && player.isLocal) jjAlert(""+player.nameUnformatted + " activated the |||Dart Trap", false, STRING::SMALL);
	}
	void process() override {
		if (isActive()) {
			timer--;
			if (timer < 1) {
				for (int i = 0; i < 4; i++) {
					jjSample(x, y + (i*32), SOUND::P2_PTOEI, 0, 0);
					if (activator.isLocal) jjSample(75*32, 58*32, SOUND::P2_PTOEI, 0, 0);
				}
			}
			if (timer & 1 == 0) {
				for (int i = 0; i < 4; i++) {
					int id = jjAddObject(OBJECT::BULLET, x, (y - 16) + (i*32), activator.playerID, CREATOR::PLAYER);
					if (id > 0) {
						auto@ obj = jjObjects[id];
						obj.age = 7;
						obj.behavior = Projectile(simple);
						obj.determineCurAnim(ANIM::CUSTOM[2], 0);
						obj.determineCurFrame();
						obj.killAnim = jjAnimSets[ANIM::AMMO] + 4;
						obj.playerHandling = HANDLING::SPECIAL;
						obj.scriptedCollisions = true;
						obj.xSpeed = -12;
						obj.ySpeed = 0.5;
						obj.direction = -1;
						obj.var[1] = 2;
						if (activator.isLocal) obj.var[2] = 1;
					}
				}
			}
		}
	}
	private float x;
	private float y;
	private jjPLAYER@ activator;
}
class RockTrap : TimedTrapBehavior {
	RockTrap(int xTile, int yTile, int width) {
		int x = xTile << 5 | 16;
		y = yTile << 5;
		order.resize(width);
		for (int i = 0; i < width; i++) {
			order[i] = x + (i << 5);
		}
		shuffle(order, jjRNG());
	}
	void activate(jjPLAYER@ player) override {
		@activator = player;
		timer = 64;
		if (!player.isLocal) jjAlert(""+player.nameUnformatted + " activated the |||Rock Trap", true, STRING::SMALL);
		if (jjIsServer && player.isLocal) jjAlert(""+player.nameUnformatted + " activated the |||Rock Trap", false, STRING::SMALL);
		if (activator.isLocal) jjSample(75*32, 58*32, SOUND::ORANGE_BOEMR, 0, 0);
		for (int i = 0; i < 8; i++) {
			jjSample((79*32 + (i*5)), y + 96, SOUND::ORANGE_BOEMR, 0, 0);
		}
	}
	void process() override {
		if (isActive()) {
			timer--;
			if (timer & 1 == 0) {
				int xCur = order[timer >> 1];
				int yCur = y;
				while (!jjMaskedPixel(xCur, yCur)) {
					yCur--;
				}
				int id = jjAddObject(OBJECT::BULLET, xCur, yCur, activator.playerID, CREATOR::PLAYER);
				if (id > 0) {
					auto@ obj = jjObjects[id];
					obj.age = 4;
					obj.behavior = Projectile(rotating);
					obj.determineCurAnim(ANIM::CUSTOM[1], 2);
					obj.determineCurFrame();
					obj.killAnim = jjAnimSets[ANIM::AMMO] + 72;
					obj.playerHandling = HANDLING::SPECIAL;
					obj.scriptedCollisions = true;
					obj.ySpeed = 1.f;
					obj.yAcc = 0.25f;
					obj.var[1] = 3;
					if (activator.isLocal) obj.var[2] = 1;
				}
			}
		}
	}
	private int y;
	private jjPLAYER@ activator;
	private array<int> order;
}
class BladeTrap : TrapBehavior {
	bool isActive() const override {
		// TODO
		return false;
	}
	void activate(jjPLAYER@ player) override {
		@activator = player;
		// TODO
	}
	void process() override {
		// TODO
	}
	private jjPLAYER@ activator;
}
class Projectile : jjBEHAVIORINTERFACE {
	Projectile(ProjectileDrawingMethod method) {
		drawingMethod = method;
	}
	void onBehave(jjOBJ@ obj) override {
		if (obj.state == STATE::START) {
			obj.state = STATE::FLY;
			obj.direction = obj.xSpeed < 0.f ? -1 : 1;
		}
		obj.counter++;
		if (obj.state == STATE::FLY) {
			obj.xPos += obj.xSpeed += obj.xAcc;
			obj.yPos += obj.ySpeed += obj.yAcc;
			if (uint(obj.counter) == obj.counterEnd || jjMaskedPixel(int(obj.xPos), int(obj.yPos))) {
				if (obj.var[2] == 1) {
					switch (obj.var[1]) {
						case 1: jjSamplePriority(SOUND::BILSBOSS_FIRE); break;
						case 2: jjSamplePriority(SOUND::COMMON_LANDCAN1); break;
						case 3: jjSamplePriority(SOUND::COMMON_BENZIN1); break;
						default: break;
					}
				} else {
					switch (obj.var[1]) {
						case 1: jjSample(obj.xPos, obj.yPos, SOUND::BILSBOSS_FIRE, 0, 0); break;
						case 2: jjSample(obj.xPos, obj.yPos, SOUND::COMMON_LANDCAN1, 0, 0); break;
						case 3: jjSample(obj.xPos, obj.yPos, SOUND::COMMON_BENZIN1, 0, 0); break;
						default: break;
					}
				}
				obj.state = STATE::KILL;
				obj.curAnim = obj.killAnim;
				obj.frameID = 0;
				obj.determineCurFrame();
			}
		}
		if (obj.counter % obj.age == 0) {
			const auto@ anim = jjAnimations[obj.curAnim];
			obj.frameID++;
			if (obj.frameID == int(anim.frameCount)) {
				if (obj.state == STATE::KILL) {
					obj.delete();
					return;
				} else {
					obj.frameID = 0;
				}
			}
			obj.determineCurFrame();
		}
	}
	void onDraw(jjOBJ@ obj) {
		if (obj.state == STATE::FLY) {
			switch (drawingMethod) {
				case simple:
					obj.draw();
					break;
				case angled: {
					int angle = int(atan2(-obj.ySpeed, obj.xSpeed) * 163.f);
					jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, angle);
					break;
				}
				case rotating:
					jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.counter << 5);
					break;
			}
		} else {
			obj.draw();
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (bullet is null && obj.state == STATE::FLY)
			killLocalPlayerWithTrap(player, obj.creatorType == CREATOR::PLAYER ? jjPlayers[obj.creatorID] : null);
		return true;
	}
	private ProjectileDrawingMethod drawingMethod;
}
class CannotBeShotDown : jjBEHAVIORINTERFACE {
	CannotBeShotDown(const jjBEHAVIOR &in behavior) {
		originalBehavior = behavior;
	}
	void onBehave(jjOBJ@ obj) {
		obj.behave(originalBehavior);
		if (obj.state == STATE::FLOATFALL)
			obj.state = STATE::FLOAT;
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (bullet is null) {
			obj.behavior = originalBehavior;
			if (player.objectHit(obj, force, obj.playerHandling))
				return true;
			obj.behavior = this;
		}
		return false;
	}
	private jjBEHAVIOR originalBehavior;
}
void LaserGun(jjOBJ@ obj) {
	jjPLAYER@ creator = jjPlayers[obj.creatorID];
	if (obj.state == STATE::START) {
		obj.state = STATE::FLY;
		obj.direction = creator.direction;
	}
	if (jjMaskedPixel(int(obj.xPos + obj.xSpeed + obj.var[7] / 65536.f), int(obj.yPos))) {
		obj.xSpeed = -obj.xSpeed;
		obj.var[7] = -obj.var[7];
		obj.xAcc = -obj.xAcc;
		obj.ySpeed -= 1.5;
		obj.counter -= 5;
		if (obj.state == STATE::FLY) randomSample(obj);
	}
	else if (jjMaskedPixel(int(obj.xPos), int(obj.yPos + obj.ySpeed))) {
		obj.ySpeed = -obj.ySpeed;
		obj.yAcc = -obj.yAcc;
		obj.xSpeed -= obj.direction == 1? -1.5:1.5;
		obj.counter -= 5;
		if (obj.state == STATE::FLY) randomSample(obj);
	}
	obj.var[0] = int(atan2(-obj.ySpeed, obj.xSpeed) * (512.f * 0.318309886142228f));
	if (obj.state == STATE::FLY) jjDrawRotatedSprite(obj.xPos, obj.yPos, ANIM::CUSTOM[0], 1, 0, obj.var[0], 0.9, 0.9, SPRITE::NORMAL);
	obj.behave(BEHAVIOR::BULLET, obj.state == STATE::FLY? false:true);
}
void LaserGunPU(jjOBJ@ obj) {
	jjPLAYER@ creator = jjPlayers[obj.creatorID];
	if (obj.state == STATE::START) {
		obj.state = STATE::FLY;
		obj.direction = creator.direction;
	}
	if (jjMaskedPixel(int(obj.xPos + obj.xSpeed + obj.var[7] / 65536.f), int(obj.yPos))) {
		obj.xSpeed = -obj.xSpeed;
		obj.var[7] = -obj.var[7];
		obj.xAcc = -obj.xAcc;
		obj.ySpeed -= 3;
		obj.counter -= 5;
		if (obj.state == STATE::FLY) randomSample(obj);
	}
	else if (jjMaskedPixel(int(obj.xPos), int(obj.yPos + obj.ySpeed))) {
		obj.ySpeed = -obj.ySpeed;
		obj.yAcc = -obj.yAcc;
		obj.xSpeed -= obj.direction == 1? -3:3;
		obj.counter -= 5;
		if (obj.state == STATE::FLY) randomSample(obj);
	}
	obj.var[0] = int(atan2(-obj.ySpeed, obj.xSpeed) * (512.f * 0.318309886142228f));
	
	if (obj.state == STATE::FLY) jjDrawRotatedSprite(obj.xPos, obj.yPos, ANIM::CUSTOM[0], 0, 0, obj.var[0], 1, 1, SPRITE::NORMAL);
	obj.behave(BEHAVIOR::BULLET, obj.state == STATE::FLY? false:true);
}
void randomSample(jjOBJ@ obj) {
	switch (jjRandom() % 4) {
		case 0: jjSample(obj.xPos, obj.yPos, SOUND::AMMO_BUL1, 0, 0); break;
		case 1: jjSample(obj.xPos, obj.yPos, SOUND::AMMO_BULFL1, 0, 0); break;
		case 2: jjSample(obj.xPos, obj.yPos, SOUND::AMMO_BULFL2, 0, 0); break;
		case 3: jjSample(obj.xPos, obj.yPos, SOUND::AMMO_BULFL3, 0, 0); break;
	}
}
void coinBooth(jjOBJ@ obj) {
	switch (obj.state) {
		case STATE::DEACTIVATE:
			obj.deactivate();
			break;
		case STATE::START: {
				int x = int(obj.xOrg) >>> 5;
				int y = int(obj.yOrg) >>> 5;
				if (jjParameterGet(x, y, 17, 1) == 0) {
					obj.delete();
					return;
				}
				obj.var[0] = jjParameterGet(x, y, 8, 8);
				obj.var[1] = jjParameterGet(x, y, 0, 8);
				obj.var[2] = jjParameterGet(x, y, 16, 1);
				obj.var[3] = jjParameterGet(x, y, 18, 1);
				const array<int> colors = {248, 48, 0, 24, 8, 32, 40};
				obj.var[4] = colors[obj.var[0] % 7];
				obj.frameID = 0;
				obj.determineCurFrame();
				obj.xPos -= 16.f;
				obj.yPos -= 16.f;
				obj.putOnGround(true);
				obj.state = STATE::STILL;
			}
			break;
		default: {
			const auto@ boothAnim = jjAnimations[obj.special];
			obj.counter++;
			if (obj.counter & 7 == 0) {
				obj.frameID++;
				if (obj.frameID >= int(jjAnimations[obj.curAnim].frameCount))
					obj.frameID = 0;
				obj.determineCurFrame();
			}
			jjDrawSpriteFromCurFrame(obj.xPos - 1.f, obj.yPos + 3.f, boothAnim + (obj.counter >> 3) % boothAnim.frameCount, obj.direction);
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame + jjAnimations[obj.curAnim].frameCount, obj.direction);
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::PALSHIFT, obj.var[4]);
			if (obj.var[0] != 0) {
				jjTEXTAPPEARANCE style;
				style.align = STRING::CENTER;
				style.yAmp = 1;
				jjDrawString(obj.xPos - 4.f, obj.yPos - 12.f, formatInt(obj.var[0]), STRING::SMALL, style, 0, SPRITE::PALSHIFT, 208);
			}
			for (int i = 0; i < jjLocalPlayerCount; i++) {
				auto@ player = jjLocalPlayers[i];
				auto@ data = playerData[i];
				if (player.isInGame && data.trapSelection == -1) {
					float xDist = player.xPos - obj.xPos;
					float yDist = player.yPos - obj.yPos;
					if (xDist * xDist + yDist * yDist < 1024.f) {
						data.trapPrice = obj.var[0];
						if (player.coins < data.trapPrice)
							player.testForCoins(data.trapPrice);
						else
							data.trapSelection = 0;
					}
				}
			}
		}
	}
}
void createMinimap(jjANIMSET@ animSet) {
	animSet.allocate(array<uint> = {1});
	auto@ frame = jjAnimFrames[jjAnimations[animSet]];
	const int shift = 1;
	const int width = jjLayerWidth[4] << 1;
	const int height = jjLayerHeight[4] << 1;
	const int cornerRadius = 32;
	jjPIXELMAP image(width, height);
	const auto@ order = jjLayerOrderGet();
	uint firstLayer = 0;
	while (firstLayer != order.length() && !hasLayer4Speed(order[firstLayer])) {
		firstLayer++;
	}
	uint lastLayer = firstLayer;
	while (lastLayer != order.length() && hasLayer4Speed(order[lastLayer])) {
		lastLayer++;
	}
	const jjPIXELMAP background(0, 0, 256, 256, 8);
	for (int i = 0; i < height; i++) {
		for (int j = 0; j < width; j++) {
			if (j < cornerRadius) {
				int xDist = cornerRadius - j;
				int yDist = 0;
				if (i < cornerRadius)
					yDist = cornerRadius - i;
				else if (i >= height - cornerRadius)
					yDist = i - (height - cornerRadius);
				if (xDist * xDist + yDist * yDist > cornerRadius * cornerRadius)
					continue;
			}
			uint layer = firstLayer;
			int color = 0;
			while (color == 0 && layer != lastLayer) {
				int tile = jjGetStaticTile(jjTileGet(layer, j >> shift, i >> shift));
				jjPIXELMAP src(tile);
				color = src[(j & 1) << 4 | 8, (i & 1) << 4 | 8];
				layer++;
			}
			if (color == 0)
				color = background[(j >> 3) % background.width, (i >> 3) % background.height];
			image[j, i] = color;
		}
	}
	image.save(frame);
}
void setUpTraps() {
	Trap trap;
	
	trap.name = "Crystal Spike Pit";
	trap.description = """A floor opens, exposing players to sharp crystal spikes.
		Players will die upon touching the Crystal Spikes.""";
	trap.x = 37 << 5;
	trap.y = 80 << 5;
	@trap.behavior = SpikeTrap(34, 81, 6, 271 ^ 376);
	traps.insertLast(trap);
	
	/*trap.name = "Spinning Blade";
	trap.description = """A spinning blade rises up from the floor, quickly moves
		to the left before stopping, then returns to its original position.
		Players will die by touching the blade.""";
	trap.x = 33 << 5;
	trap.y = 47 << 5;
	@trap.behavior = BladeTrap();
	traps.insertLast(trap);*/
	
	trap.name = "Poison Darts";
	trap.description = """Once activated, poisoned darts quickly shoot out of the wall.
		Players hit by poisoned darts will die.""";
	trap.x = 69 << 5;
	trap.y = 31 << 5;
	@trap.behavior = DartTrap(69, 31);
	traps.insertLast(trap);
	
	trap.name = "Volcano";
	trap.description = """Fireballs quickly shoot up from the temple roof, then rain down.
		Players will die by touching the Fireballs.""";
	trap.x = 88 << 5;
	trap.y = 9 << 5;
	@trap.behavior = VolcanoTrap(88, 11);
	traps.insertLast(trap);
	
	trap.name = "Fire Wall";
	trap.description = """Flames shoot out of the wall, then dissipate after 3 seconds.
		Players will die by touching the flames.""";
	trap.x = 125 << 5;
	trap.y = 38 << 5;
	@trap.behavior = FireTrap(118, 37, 4);
	traps.insertLast(trap);
	
	trap.name = "Falling Rocks";
	trap.description = """Rocks fall haphazardly. Players will die by being crushed by the rocks.""";
	trap.x = 97 << 5;
	trap.y = 72 << 5;
	@trap.behavior = RockTrap(81, 71, 32);
	traps.insertLast(trap);
}
bool hasLayer4Speed(const jjLAYER@ layer) {
	return layer.xSpeed == 1.f && layer.ySpeed == 1.f && layer.xAutoSpeed == 0.f && layer.yAutoSpeed == 0.f;
}
void killLocalPlayerWithTrap(jjPLAYER@ player, jjPLAYER@ activator) {
	if (player.health > 0)
		player.hurt(player.health, true, activator);
}
void shuffle(array<int>& input, jjRNG& rng) {
	for (uint i = input.length(); i != 0;) {
		int index = rng() % i;
		i--;
		auto temp = input[index];
		input[index] = input[i];
		input[i] = temp;
	}
}
bool onDrawAmmo(jjPLAYER@ player, jjCANVAS@ canvas) {
	const auto@ data = playerData[player.localPlayerID];
	if (data.trapSelection >= 0) {
		const auto@ minimap = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::CUSTOM[3]]]];
		const int xMap = jjSubscreenWidth - minimap.width;
		const int yMap = jjSubscreenHeight - 96 - minimap.height;
		const float levelWidth = jjLayerWidth[4] << 5;
		const float levelHeight = jjLayerHeight[4] << 5;
		canvas.drawSprite(xMap, yMap, ANIM::CUSTOM[3], 0, 0);
		for (int i = traps.length(); i-- != 0;) {
			const auto@ target = traps[i];
			int xTrap = xMap + int(target.x / levelWidth * minimap.width);
			int yTrap = yMap + int(target.y / levelHeight * minimap.height);
			canvas.drawResizedSprite(xTrap, yTrap, ANIM::FLAG, i == data.trapSelection ? 3 : 7, 0, 0.5f, 0.5f);
		}
		for (int i = 0; i < 32; i++) {
			const auto@ target = jjPlayers[i];
			if (target.isInGame) {
				int xPlayer = xMap + int(target.xPos / levelWidth * minimap.width);
				int yPlayer = yMap + int(target.yPos / levelHeight * minimap.height);
				int face = 3;
				switch (target.charCurr) {
					case CHAR::BIRD:
						face = 0;
						break;
					case CHAR::BIRD2:
						face = 1;
						break;
					case CHAR::FROG:
						face = 2;
						break;
					case CHAR::JAZZ:
						face = 3;
						break;
					case CHAR::LORI:
						face = 4;
						break;
					case CHAR::SPAZ:
						face = jjIsTSF ? 5 : 4;
						break;
				}
				canvas.drawResizedSprite(xPlayer, yPlayer, ANIM::FACES, face, 0, 0.5f, 0.5f, SPRITE::PLAYER, i);
			}
		}
		const auto@ trap = traps[data.trapSelection];
		jjTEXTAPPEARANCE style;
		style.align = STRING::CENTER;
		int middle = jjSubscreenWidth / 2;
		canvas.drawString(middle, 64, "Activate a Trap?", STRING::LARGE, style);
		canvas.drawString(middle, 128, trap.name, STRING::MEDIUM, style);
		canvas.drawString(middle, 160, trap.description, STRING::SMALL, style);
		canvas.drawString(middle, jjSubscreenHeight - 64, "Press fire to activate a trap (costs 25 coins)", STRING::SMALL, style);
		canvas.drawString(middle, jjSubscreenHeight - 48, "Use the arrow keys to switch between traps", STRING::SMALL, style);
		canvas.drawString(middle, jjSubscreenHeight - 32, "Press backspace to quit", STRING::SMALL, style);
		if (trap.behavior.isActive())
			canvas.drawString(middle, jjSubscreenHeight / 2, "Trap already active", STRING::MEDIUM, style, 0, SPRITE::PALSHIFT, 216);
	}
	return weaponHook.drawAmmo(player, canvas);
}
void onLevelBegin() {
	for (int i = 1; i < 255; i++) {
		jjOBJ@ preset = jjObjectPresets[i];
		if (preset.playerHandling == HANDLING::PICKUP) {
			preset.behavior = CannotBeShotDown(preset.behavior);
		}
	}
	
	jjANIMATION@ anim = jjAnimations[jjAnimSets[ANIM::CUSTOM[0]] + 4];
	for (uint i = 0; i < anim.frameCount; ++i) {
		jjANIMFRAME@ frame = jjAnimFrames[anim + i];
		jjPIXELMAP sprite(frame);
		for (uint x = 0; x < sprite.width; ++x)
			for (uint y = 0; y < sprite.height; ++y)
			if (sprite[x,y] < 8 && x <= 16 && y >= 2) sprite[x,y] = 71;
		sprite.save(frame);
	}
	
	jjSampleLoad(SOUND::P2_PTOEI, "bow_fire_arrow.wav");
	jjSampleLoad(SOUND::ORANGE_BOEMR, "expmine.wav");
	jjSampleLoad(SOUND::COMMON_COLLAPS, "floorslide.wav");
	jjSampleLoad(SOUND::CATERPIL_RIDOE, "floorslide.wav");
}
void onLevelLoad() {
	createMinimap(jjAnimSets[ANIM::CUSTOM[3]]);
	setUpTraps();
	jjUseLayer8Speeds = true;      
	se::roller.loadAnims(jjAnimSets[ANIM::CUSTOM[2]]);
	se::roller.loadSamples(array<SOUND::Sample> = {SOUND::INTRO_BRAKE});
	se::roller.setAsWeapon(2, weaponHook);
	jjAnimSets[ANIM::CUSTOM[2]].load(0, "arrowgun.j2a");
	jjAnimSets[ANIM::CUSTOM[1]].load(0, "meteor.j2a");
	jjAnimSets[ANIM::CUSTOM[0]].load(0, "LaserBlaster.j2a");
	jjAnimations[jjAnimSets[ANIM::AMMO] + 29] = jjAnimations[jjAnimSets[ANIM::CUSTOM[0]] + 2];
	jjAnimations[jjAnimSets[ANIM::AMMO] + 28] = jjAnimations[jjAnimSets[ANIM::CUSTOM[0]] + 3];
	jjAnimations[jjAnimSets[ANIM::PICKUPS] + 62] = jjAnimations[jjAnimSets[ANIM::CUSTOM[0]] + 4];
	
	jjObjectPresets[OBJECT::ICEBULLET].behavior = LaserGun;
	jjObjectPresets[OBJECT::ICEBULLET].special = jjObjectPresets[OBJECT::ICEBULLET].determineCurAnim(ANIM::CUSTOM[0], 0);
	jjObjectPresets[OBJECT::ICEBULLET].xSpeed = 1.5;
	jjObjectPresets[OBJECT::ICEBULLET].counterEnd = 125;
	jjObjectPresets[OBJECT::ICEBULLET].freeze = 0;
	jjObjectPresets[OBJECT::ICEBULLET].eventID = OBJECT::FIREBALLBULLET;
	jjObjectPresets[OBJECT::ICEBULLET].lightType = jjObjectPresets[OBJECT::FIREBALLBULLET].lightType;

	jjObjectPresets[OBJECT::ICEBULLETPU].behavior = LaserGunPU;
	jjObjectPresets[OBJECT::ICEBULLETPU].special = jjObjectPresets[OBJECT::ICEBULLET].determineCurAnim(ANIM::CUSTOM[0], 1);
	jjObjectPresets[OBJECT::ICEBULLETPU].xSpeed = 2;
	jjObjectPresets[OBJECT::ICEBULLETPU].counterEnd = 95;
	jjObjectPresets[OBJECT::ICEBULLETPU].var[6] = 8;
	jjObjectPresets[OBJECT::ICEBULLETPU].freeze = 0;
	jjObjectPresets[OBJECT::ICEBULLETPU].eventID = OBJECT::FIREBALLBULLETPU;
	jjObjectPresets[OBJECT::ICEBULLET].lightType = jjObjectPresets[OBJECT::FIREBALLBULLET].lightType;
	
	jjObjectPresets[OBJECT::ICEPOWERUP].determineCurAnim(ANIM::CUSTOM[0], 4, true);
	jjObjectPresets[OBJECT::ICEPOWERUP].determineCurFrame();
	
	jjObjectPresets[OBJECT::WARP].behavior = coinBooth;
	
	jjWeapons[WEAPON::ICE].spread = SPREAD::NORMAL;
	jjSampleLoad(SOUND::AMMO_ICEGUN, "f_laser1.wav");
	jjSampleLoad(SOUND::AMMO_ICEGUN2, "f_laser2.wav");
	jjSampleLoad(SOUND::AMMO_ICEGUNPU, "f_laser2.wav");
	jjSampleLoad(SOUND::AMMO_ICEPU1, "f_laser3.wav");
	jjSampleLoad(SOUND::AMMO_ICEPU2, "f_laser3.wav");
	jjSampleLoad(SOUND::AMMO_ICEPU3, "f_laser4.wav");
	jjSampleLoad(SOUND::AMMO_ICEPU4, "f_laser4.wav");

	jjANIMATION@ anim = jjAnimations[jjAnimSets[ANIM::AMMO] + 82];
	for (uint i = 0; i < anim.frameCount; ++i) {
		jjANIMFRAME@ frame = jjAnimFrames[anim + i];
		jjPIXELMAP sprite(frame);
		for (uint x = 0; x < sprite.width; ++x) {
			for (uint y = 0; y < sprite.height; ++y) {
				if (sprite[x, y] != 0 && sprite[x, y] != 15)
					sprite[x, y] += 33;
			}
		}
		sprite.save(frame);
	}
}
void onMain() {
	for (int i = traps.length(); i-- != 0;) {
		traps[i].behavior.process();
	}
	weaponHook.processMain();
	array<jjLAYER@> layers = jjLayerOrderGet();
	layers[8].xAutoSpeed = layers[9].xAutoSpeed = 0;
	layers[8].xOffset += 0.2f;
	layers[9].xOffset += 0.08f;
}
void onPlayer(jjPLAYER@ player) {
	auto@ data = playerData[player.localPlayerID];
	if (data.trapSelection >= 0) {
		if (!player.isInGame || player.health == 0 || jjKey[quitTrapSelectionKey]) {
			data.trapSelection = -210;
			player.cameraUnfreeze();
		} else {
			const auto@ trap = traps[data.trapSelection];
			player.cameraFreeze(trap.x, trap.y, true, true);
			player.showText("");
		}
	} else if (data.trapSelection < -1) {
		data.trapSelection++;
	}
	weaponHook.processPlayer(player);
	if (jjGameMode == GAME::SP && jjGameTicks == 1) {
		player.warpToTile(73,57,true);
	}
}
void onPlayerInput(jjPLAYER@ player) {
	auto@ data = playerData[player.localPlayerID];
	if (data.trapSelection >= 0) {
		if (player.keyLeft && !data.prevKeyLeft) {
			data.trapSelection--;
			if (data.trapSelection < 0)
				data.trapSelection = traps.length() - 1;
		}
		if (player.keyRight && !data.prevKeyRight) {
			data.trapSelection++;
			if (data.trapSelection >= int(traps.length()))
				data.trapSelection = 0;
		}
		if (player.keyFire && !data.prevKeyFire) {
			auto@ trap = traps[data.trapSelection];
			if (!trap.behavior.isActive() && player.testForCoins(data.trapPrice)) {
				trap.behavior.activate(player);
				jjSTREAM packet;
				packet.push(uint8(trapPacket));
				packet.push(uint8(data.trapSelection));
				packet.push(uint8(player.playerID));
				jjSendPacket(packet);
			}
		}
	}
	data.prevKeyFire = player.keyFire;
	data.prevKeyLeft = player.keyLeft;
	data.prevKeyRight = player.keyRight;
	if (data.trapSelection >= 0) {
		player.keyDown = false;
		player.keyFire = false;
		player.keyLeft = false;
		player.keyJump = false;
		player.keyRight = false;
		player.keyRun = false;
		player.keyUp = false;
	}
	weaponHook.processPlayerInput(player);
}
void onReceive(jjSTREAM &in packet, int clientID) {
	if (!weaponHook.processPacket(packet, clientID)) {
		jjSTREAM copy = packet;
		uint8 type;
		uint8 trapID;
		uint8 playerID;
		if (packet.pop(type) && packet.pop(trapID) && packet.pop(playerID)) {
			if (type == trapPacket && trapID < traps.length() && playerID < 32) {
				auto@ player = jjPlayers[playerID];
				if (!jjIsServer || player.clientID == clientID) {
					traps[trapID].behavior.activate(player);
					if (jjIsServer)
						jjSendPacket(copy, -clientID);
				}
			}
		}
	}
}