Downloads containing HH24_level04.j2as

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Holiday Hare 24Featured Download PurpleJazz Single player 10 Download file

File preview

#pragma require "HH18Smoke_HH24.asc"
#pragma require "HH24.asc"
#pragma require "SEroller.asc"
#pragma require "TrueColor v13.asc"
#pragma require "AlienPalace.j2t"
#pragma require "HH18E1.j2a"
#pragma require "SEhh24.j2a"
#pragma require "SEhh24dryfire.wav"
#pragma require "SEhh24launcher1.wav"
#pragma require "SEhh24launcher2.wav"
#pragma require "SEhh24launcherswitch.wav"
#pragma require "SEhh24secret.wav"
#pragma require "SEhh24spikeball1.wav"
#pragma require "SEhh24spikeball2.wav"
#pragma require "SEwaterbubble.png"
#pragma require "SExmas.j2a"
#include "HH18Smoke_HH24.asc"
#include "HH24.asc"
#include "SEroller.asc"
#include "TrueColor v13.asc"
///@Event 103=Frozen Alien      |-|Enemy    |Frozen |Alien
///@Event 104=Penguin Gentleman |-|Enemy    |Pengu  |Boyo
///@Event 215=Spike Ball        |+|Object   |Spike  |Ball
///@Event 232=Bubble Launcher   |+|Object   |Bubble |Launch |Angle 1:3 |Angle 2:3 |Switch: 4
///@Event 254=Delet This        |-|Modifier |NOT    |v
namespace sound {
	const SOUND::Sample roller = SOUND::INTRO_BLOW;
	const SOUND::Sample spikeballHit = SOUND::INTRO_BOEM1;
	const SOUND::Sample spikeballRecover = SOUND::INTRO_BOEM2;
	const SOUND::Sample launcherSwitch = SOUND::INTRO_BRAKE;
	const SOUND::Sample dryFire = SOUND::INTRO_END;
	const SOUND::Sample secret = SOUND::INTRO_GRAB;
	const SOUND::Sample launcherHit = SOUND::INTRO_GREN1;
	const SOUND::Sample launcherFire = SOUND::INTRO_GREN2;
}
enum CustomEvent {
	event_launcher = 232,
	event_floor_spike = 252,
	event_ceiling_spike = 253,
	event_deleter = 254,
}
enum CustomAnim {
	anim_roller,
	anim_bubble,
	anim_penguin,
	anim_rope,
	anim_launcher,
	anim_spike_ball,
	anim_poster,
	anim_temp_penguin,
	anim_temp_pickups,
}
enum BubbleVar {
	bubble_size,
	bubble_max_size
}
enum LauncherVar {
	launcher_angle_1,
	launcher_angle_2,
	launcher_angle_target,
	launcher_angle_alt,
	launcher_angle_displayed,
	launcher_angular_velocity,
	launcher_state,
	launcher_liquid_deficit
}
enum LauncherSwitchVar {
	launcher_switch_rope_length,
	launcher_switch_rope_frame
}
class Bubble : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) override {
		obj.state = STATE::FLOAT;
		obj.xPos += obj.xSpeed;
		obj.yPos += obj.ySpeed;
		float squish = 1.f + 0.075f * jjSin(obj.counter);
		obj.counter += 9;
		if (obj.var[bubble_size] < obj.var[bubble_max_size])
			obj.var[bubble_size] = obj.var[bubble_size] + 1;
		float scale = 1.f * obj.var[bubble_size] / obj.var[bubble_max_size];
		if (isOnScreen(obj)) {
			if (jjColorDepth != 8)
				TrueColor::DrawResizedSprite(obj.xPos, obj.yPos, ANIM::CUSTOM[anim_bubble], 0, 1, scale * squish, scale / squish, 3);
			else
				jjDrawResizedSprite(obj.xPos, obj.yPos, ANIM::CUSTOM[anim_bubble], 0, 0, scale * squish, scale / squish, SPRITE::TRANSLUCENT, 0, 3);
		}
		bool pop = false;
		int width = jjLayerWidth[4] << 5;
		int height = jjLayerHeight[4] << 5;
		float outOfBounds = 2 * obj.var[1];
		pop = obj.xPos < -outOfBounds || obj.yPos < -outOfBounds || obj.xPos > width + outOfBounds || obj.yPos > height + outOfBounds;
		int radius = obj.var[0] * 3 / 4;
		for (int i = 0; !pop && i < 1024; i += 32) {
			int x = int(obj.xPos + jjSin(i) * radius);
			int y = int(obj.yPos + jjCos(i) * radius);
			pop = x >= 0 && y >= 0 && x < width && y < height && jjMaskedPixel(x, y) && jjEventAtLastMaskedPixel != AREA::STOPENEMY;
		}
		if (pop)
			destroy(obj);
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (bullet !is null) {
			int radius = obj.var[bubble_size] * 3 / 4;
			float x = bullet.xPos - obj.xPos;
			float y = bullet.yPos - obj.yPos;
			if (x * x + y * y < radius * radius)
				destroy(obj);
		}
		return true;
	}
	void assign(jjOBJ@ obj) {
		obj.behavior = this;
		obj.counter = 0;
		obj.var[bubble_size] = 2;
		obj.var[bubble_max_size] = 64;
		obj.playerHandling = HANDLING::SPECIAL;
		obj.bulletHandling = HANDLING::DETECTBULLET;
		obj.deactivates = false;
		obj.scriptedCollisions = true;
		obj.isBlastable = false;
		obj.isFreezable = false;
		obj.isTarget = false;
		obj.causesRicochet = false;
		obj.frameID = 0;
		obj.determineCurAnim(ANIM::CUSTOM[anim_bubble], 0);
		obj.determineCurFrame();
	}
	void destroy(jjOBJ@ obj) const {
		int count = obj.var[bubble_size] * 4;
		for (int i = 0; i < count; i++) {
			auto@ part = jjAddParticle(PARTICLE::PIXEL);
			if (part !is null) {
				uint random = jjRandom();
				int angle = random & 1023;
				random >>= 10;
				float z = (random & 63) / 64.f;
				random >>= 6;
				float r = sqrt(1.f - z * z) * float(obj.var[bubble_size]);
				float xComp = jjSin(angle);
				float yComp = jjCos(angle);
				part.xPos = obj.xPos + xComp * r;
				part.yPos = obj.yPos + yComp * r;
				part.xSpeed = xComp + ((random & 15) - 7.5f) * 0.05f;
				random >>= 4;
				part.ySpeed = yComp + ((random & 15) - 7.5f) * 0.05f;
				random >>= 4;
				part.pixel.size = 1;
				for (int j = 0; j < 4; j++) {
					part.pixel.color[j] = (jjColorDepth != 8 ? 33 : 35) + (random & 3);
					random >>= 2;
				}
			}
		}
		jjSample(obj.xPos, obj.yPos, SOUND::COMMON_PLOP2);
		obj.delete();
	}
	bool isOnScreen(const jjOBJ@ obj) const {
		float margin = obj.var[bubble_size] * 2;
		float x = obj.xPos;
		float y = obj.yPos;
		for (int i = 0; i < jjLocalPlayerCount; i++) {
			const auto@ player = jjLocalPlayers[i];
			float left = player.cameraX;
			float top = player.cameraY;
			float right = left + jjSubscreenWidth;
			float bottom = top + jjSubscreenHeight;
			if (x + margin >= left && x - margin <= right && y + margin >= top && y - margin <= bottom)
				return true;
		}
		return false;
	}
}
class Launcher : jjBEHAVIORINTERFACE {
	float bubbleSpeed = 2.f;
	int fireTime = 64;
	int rechargeTime = 120;
	void onBehave(jjOBJ@ obj) override {
		float xScale = 1.f;
		float yScale = 1.f;
		if (obj.state == STATE::START) {
			obj.state = STATE::IDLE;
			int x = int(obj.xPos) >> 5;
			int y = int(obj.yPos) >> 5;
			obj.var[launcher_angle_1] = jjParameterGet(x, y, 0, 3) << 7;
			obj.var[launcher_angle_2] = jjParameterGet(x, y, 3, 3) << 7;
			if (iabs(obj.var[launcher_angle_2] - obj.var[launcher_angle_1]) > 512) {
				auto lesser = obj.var[launcher_angle_1] < obj.var[launcher_angle_2] ? launcher_angle_1 : launcher_angle_2;
				obj.var[lesser] = obj.var[lesser] + 1024;
			}
			obj.var[launcher_angle_target] = obj.var[launcher_angle_1];
			obj.var[launcher_angle_alt] = obj.var[launcher_angle_2];
			obj.var[launcher_angle_displayed] = obj.var[launcher_angle_target];
			int ropeLength = jjParameterGet(x, y, 6, 4) << 5;
			if (ropeLength != 0) {
				int id = jjAddObject(OBJECT::TRIGGERCRATE, obj.xPos, obj.yPos + ropeLength, obj.objectID, CREATOR::OBJECT, BEHAVIOR::INACTIVE);
				if (id > 0) {
					jjOBJ@ other = jjObjects[id];
					launcherSwitch.assign(other);
					other.var[launcher_switch_rope_length] = ropeLength;
				}
			}
		}
		if (obj.counter > 0) {
			float squish = 1.f + 0.3f * jjSin(obj.counter * 1024 / fireTime);
			xScale *= 1.f / squish;
			yScale *= squish;
			if (obj.var[launcher_liquid_deficit] < rechargeTime) {
				obj.var[launcher_liquid_deficit] = obj.var[launcher_liquid_deficit] + obj.counter / 4;
				if (obj.var[launcher_liquid_deficit] > rechargeTime)
					obj.var[launcher_liquid_deficit] = rechargeTime;
			}
			if (obj.counter == fireTime * 3 / 4) {
				float xOff = jjCos(obj.var[launcher_angle_target]);
				float yOff = -jjSin(obj.var[launcher_angle_target]);
				int id = jjAddObject(OBJECT::BUBBLE, obj.xPos + 40.f * xOff, obj.yPos + 40.f * yOff, obj.objectID, CREATOR::OBJECT, BEHAVIOR::INACTIVE);
				if (id > 0) {
					jjOBJ@ other = jjObjects[id];
					bubble.assign(other);
					other.xSpeed = xOff * bubbleSpeed;
					other.ySpeed = yOff * bubbleSpeed;
					jjSample(other.xPos, other.yPos, sound::launcherFire);
				}
			}
			obj.counter++;
			if (obj.counter >= fireTime)
				obj.counter = 0;
		} else if (obj.var[launcher_liquid_deficit] > 0) {
			obj.var[launcher_liquid_deficit] = obj.var[launcher_liquid_deficit] - 1;
		}
		if (obj.state == STATE::ROTATE) {
			int direction = isign(obj.var[launcher_angle_target] - obj.var[launcher_angle_displayed]);
			obj.var[launcher_angular_velocity] = obj.var[launcher_angular_velocity] + direction;
			if (obj.var[launcher_angular_velocity] < -16)
				obj.var[launcher_angular_velocity] = -16;
			else if (obj.var[launcher_angular_velocity] > 16)
				obj.var[launcher_angular_velocity] = 16;
			obj.var[launcher_angle_displayed] = obj.var[launcher_angle_displayed] + obj.var[launcher_angular_velocity];
			int newDirection = isign(obj.var[launcher_angle_target] - obj.var[launcher_angle_displayed]);
			if (newDirection != direction) {
				obj.var[launcher_angular_velocity] = -obj.var[launcher_angular_velocity] * 5 / 8;
				obj.var[launcher_angle_displayed] = obj.var[launcher_angle_target] + obj.var[launcher_angular_velocity];
				if (obj.var[launcher_angular_velocity] == 0)
					obj.state = STATE::IDLE;
			}
		}
		if (isOnScreen(obj)) {
			SPRITE::Mode mode = obj.justHit > 0 ? SPRITE::SINGLECOLOR : SPRITE::NORMAL;
			int liquidFrame = obj.var[launcher_liquid_deficit] * jjAnimations[obj.curAnim + 3].frameCount / (rechargeTime + 1);
			jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, jjAnimations[obj.curAnim + 2], obj.var[launcher_angle_displayed], xScale, yScale, mode, 15);
			if (mode != SPRITE::SINGLECOLOR)
				jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, jjAnimations[obj.curAnim + 3] + liquidFrame, 0, SPRITE::TRANSLUCENT);
			jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, jjAnimations[obj.curAnim + 1], obj.var[launcher_angle_displayed], xScale, yScale, mode, 15);
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (bullet !is null && obj.counter == 0 && obj.var[launcher_liquid_deficit] == 0) {
			obj.justHit = 5;
			obj.counter++;
			bullet.state = STATE::EXPLODE;
			jjSample(obj.xPos, obj.yPos, sound::launcherHit);
		}
		return true;
	}
	void assign(jjOBJ@ obj) {
		obj.behavior = this;
		obj.playerHandling = HANDLING::SPECIAL;
		obj.bulletHandling = HANDLING::DETECTBULLET;
		obj.deactivates = false;
		obj.scriptedCollisions = true;
		obj.isBlastable = false;
		obj.isFreezable = false;
		obj.isTarget = false;
		obj.causesRicochet = false;
		obj.frameID = 0;
		obj.determineCurAnim(ANIM::CUSTOM[anim_launcher], 0);
		obj.determineCurFrame();
		obj.killAnim = jjAnimations[obj.determineCurAnim(ANIM::CUSTOM[anim_launcher], 1, false)];
	}
	bool isOnScreen(const jjOBJ@ obj) const {
		const float margin = 60.f;
		float x = obj.xPos;
		float y = obj.yPos;
		for (int i = 0; i < jjLocalPlayerCount; i++) {
			const auto@ player = jjLocalPlayers[i];
			float left = player.cameraX;
			float top = player.cameraY;
			float right = left + jjSubscreenWidth;
			float bottom = top + jjSubscreenHeight;
			if (x + margin >= left && x - margin <= right && y + margin >= top && y - margin <= bottom)
				return true;
		}
		return false;
	}
}
class LauncherSwitch : jjBEHAVIORINTERFACE {
	int revolutionCount = 2;
	int spinTime = 110;
	float angularVelocity = (2 * revolutionCount + 1) * 1024 / float(spinTime);
	float angularAcceleration = angularVelocity / spinTime;
	void onBehave(jjOBJ@ obj) override {
		int angle = 0;
		if (obj.state == STATE::START) {
			obj.direction = 1;
			obj.state = STATE::IDLE;
		} else if (obj.state == STATE::ROTATE) {
			int oldAngle = int(0.5f * angularAcceleration * (obj.counter * obj.counter)) & 1023;
			obj.counter--;
			if (obj.counter <= 0) {
				obj.state = STATE::IDLE;
			} else {
				angle = int(0.5f * angularAcceleration * (obj.counter * obj.counter)) & 1023;
				float product = jjSin(angle) * jjSin(oldAngle);
				if (product < 0.f || product == 0.f && jjSin(angle) != 0.f)
					jjSample(obj.xPos, obj.yPos, sound::launcherSwitch, 63, 20000 + 2000 * obj.counter);
			}
		}
		int length = obj.var[launcher_switch_rope_length];
		jjDrawSwingingVineSpriteFromCurFrame(obj.xPos, obj.yPos - length, obj.var[launcher_switch_rope_frame], length, 0, SPRITE::NORMAL, 0, 5);
		SPRITE::Mode mode = obj.justHit > 0 ? SPRITE::SINGLECOLOR : SPRITE::NORMAL;
		float xScale = abs(jjCos(angle));
		jjDrawResizedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, xScale, 1.f, mode, 15);
		const auto@ creator = jjObjects[obj.creatorID];
		int frame = jjAnimations[obj.curAnim + 1] + (creator.var[angle < 256 || angle >= 768 ? launcher_angle_target : launcher_angle_alt] >> 7 & 7);
		jjDrawResizedSpriteFromCurFrame(obj.xPos, obj.yPos, frame, xScale, 1.f, mode, 15);
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (bullet !is null && obj.state == STATE::IDLE) {
			obj.justHit = 5;
			bullet.state = STATE::EXPLODE;
			obj.state = STATE::ROTATE;
			obj.counter = spinTime;
			auto@ creator = jjObjects[obj.creatorID];
			int prevAngle = creator.var[launcher_angle_target];
			creator.var[launcher_angle_target] = creator.var[launcher_angle_alt];
			creator.var[launcher_angle_alt] = prevAngle;
			creator.state = STATE::ROTATE;
		}
		return true;
	}
	void assign(jjOBJ@ obj) {
		obj.behavior = this;
		obj.playerHandling = HANDLING::SPECIAL;
		obj.bulletHandling = HANDLING::DETECTBULLET;
		obj.deactivates = false;
		obj.scriptedCollisions = true;
		obj.isBlastable = false;
		obj.isFreezable = false;
		obj.isTarget = false;
		obj.causesRicochet = false;
		obj.frameID = 0;
		obj.determineCurAnim(ANIM::CUSTOM[anim_launcher], 4);
		obj.determineCurFrame();
		obj.var[launcher_switch_rope_frame] = jjAnimations[jjAnimSets[ANIM::CUSTOM[anim_rope]]];
	}
}
class SpikeBall : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) override {
		if (obj.state == STATE::START) {
			obj.state = STATE::ATTACK;
		} else if (obj.state == STATE::HIDE) {
			const int delay = 2;
			if (obj.counter % delay == 0) {
				if (obj.frameID > obj.counter / delay)
					obj.frameID--;
				else if (obj.frameID < int(jjAnimations[obj.curAnim].frameCount) - 1)
					obj.frameID++;
			}
			obj.causesRicochet = true;
			obj.counter--;
			if (obj.counter <= 0) {
				jjSample(obj.xPos, obj.yPos, sound::spikeballRecover);
				obj.state = STATE::ATTACK;
				obj.frameID = 0;
				obj.causesRicochet = false;
			}
		}
		obj.determineCurFrame();
		obj.draw();
		if (obj.justHit == 0) {
			const auto@ anim = jjAnimations[obj.curAnim + 1];
			int frameCount = anim.frameCount;
			int timerFrame = obj.counter * frameCount / (getMaxCounter() - 5);
			if (timerFrame >= frameCount)
				timerFrame = frameCount - 1;
			jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, anim + timerFrame);
		}
	}
	bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
		if (bullet !is null) {
			if (obj.state == STATE::ATTACK)
				jjSample(obj.xPos, obj.yPos, sound::spikeballHit);
			if (!obj.causesRicochet)
				bullet.state = STATE::EXPLODE;
			obj.justHit = 5;
			obj.state = STATE::HIDE;
			obj.counter = getMaxCounter();
		} else if (obj.state == STATE::ATTACK) {
			player.hurt();
		}
		return true;
	}
	int getMaxCounter() const {
		return jjDifficulty < 3 ? 420 - jjDifficulty * 100 : 160;
	}
	void assign(jjOBJ@ obj) {
		obj.behavior = this;
		obj.playerHandling = HANDLING::SPECIAL;
		obj.bulletHandling = HANDLING::DETECTBULLET;
		obj.deactivates = false;
		obj.scriptedCollisions = true;
		obj.isBlastable = false;
		obj.isFreezable = false;
		obj.isTarget = false;
		obj.causesRicochet = false;
		obj.frameID = 0;
		obj.determineCurAnim(ANIM::CUSTOM[anim_spike_ball], 0);
		obj.determineCurFrame();
	}
}
class Poster : jjBEHAVIORINTERFACE {
	void onBehave(jjOBJ@ obj) override {
		jjDrawResizedSpriteFromCurFrame(obj.xPos, obj.yPos + 2.f, obj.curFrame, 1.05f, 1.1f, SPRITE::SHADOW);
		obj.draw();
	}
	void assign(jjOBJ@ obj) {
		obj.behavior = this;
		obj.playerHandling = HANDLING::PARTICLE;
		obj.bulletHandling = HANDLING::IGNOREBULLET;
		obj.deactivates = true;
		obj.scriptedCollisions = false;
		obj.isBlastable = false;
		obj.isFreezable = false;
		obj.isTarget = false;
		obj.causesRicochet = false;
		obj.frameID = 0;
		obj.determineCurAnim(ANIM::CUSTOM[anim_poster], 0);
		obj.determineCurFrame();
	}
}
class AlienPalaceSpikeImporter {
	private int tilesetEnd;
	private int width;
	private int height;
	void initialize() {
		const auto@ layer = jjLayers[4];
		tilesetEnd = jjTileCount;
		width = layer.width;
		height = layer.height;
	}
	array<uint8>@ makeMapping() const {
		array<uint8> mapping(256);
		mapping[144] = 15;
		for (int i = 0; i < 8; i++) {
			mapping[145 + i] = 32 + i;
		}
		return mapping;
	}
	void squarify(jjPIXELMAP& image) const {
		int ia = image[0, 0] == 0 || image[31, 0] == 0 ? 0 : 24;
		int iz = image[0, 31] == 0 || image[31, 31] == 0 ? 32 : 8;
		int ja = image[0, 0] == 0 || image[0, 31] == 0 ? 0 : 24;
		int jz = image[31, 0] == 0 || image[31, 31] == 0 ? 32 : 8;
		for (int i = ia; i < iz; i++) {
			int im = i < 16 ? 7 : 24;
			for (int j = ja; j < jz; j++) {
				int jm = j < 16 ? 7 : 24;
				image[j, i] = iabs(i - im) < iabs(j - jm) ? image[j, im] :  image[jm, i];
			}
		}
	}
	void generateCaveSpikes() const {
		jjPIXELMAP cave(132);
		for (int i = 0; i < 2; i++) {
			int src = tilesetEnd + (i == 0 ? 7 : 27);
			int dest = src - 3;
			jjPIXELMAP image(src);
			for (int j = 0; j < 32; j++) {
				for (int k = 0; k < 32; k++) {
					if (image[k, j] == 0)
						image[k, j] = cave[k, j];
				}
			}
			image.save(dest);
			jjMASKMAP(src).save(dest);
		}
	}
	void generateCorners() const {
		jjMASKMAP masked(true);
		for (int i = 0; i < 5; i++) {
			jjPIXELMAP image(51 + i);
			squarify(image);
			image.save(tilesetEnd + 11 + i);
			masked.save(tilesetEnd + 11 + i);
		}
	}
	void placeSpikes() const {
		bool needsGenerate = true;
		for (int i = 1; i < height - 1; i++) {
			for (int j = 0; j < width; j++) {
				if (j & 3 == 0)
					needsGenerate = true;
				int tile = jjTileGet(4, j, i);
				int replacement = -1;
				switch (tile) {
					case 59: {
						int below = jjTileGet(4, j, i + 1);
						if (below != 132)
							below = 0;
						replacement = jjEventGet(j, i) == AREA::HURT ? tilesetEnd + (below != 132 ? 7 : 4) : below;
						break;
					}
					case 69: {
						int above = jjTileGet(4, j, i - 1);
						if (above != 132)
							above = 0;
						replacement = jjEventGet(j, i) == AREA::HURT ? tilesetEnd + (above != 132 ? 27 : 24) : above;
						break;
					}
				}
				if (replacement >= 0) {
					if (needsGenerate) {
						jjGenerateSettableTileArea(4, j, i, 1, 1);
						needsGenerate = false;
					}
					jjTileSet(4, j, i, replacement);
				}
			}
		}
	}
	void placeCorners() const {
		bool needsGenerate = true;
		for (int i = 1; i < height - 1; i++) {
			for (int j = 0; j < width; j++) {
				if (j & 3 == 0)
					needsGenerate = true;
				int tile = jjTileGet(4, j, i);
				int replacement = -1;
				switch (tile) {
					case 51: case 53: case 180: case 181: {
						int below = jjTileGet(4, j, i + 1);
						if (below == tilesetEnd + 4 || below == tilesetEnd + 7)
							replacement = (tile & 127) + tilesetEnd - (tile != 180 ? 40 : 41);
						break;
					}
					case 54: case 55: case 182: case 183: {
						int above = jjTileGet(4, j, i - 1);
						if (above == tilesetEnd + 24 || above == tilesetEnd + 27)
							replacement = (tile & 127) + tilesetEnd - 40;
						break;
					}
				}
				if (replacement >= 0) {
					if (needsGenerate) {
						jjGenerateSettableTileArea(4, j, i, 1, 1);
						needsGenerate = false;
					}
					jjTileSet(4, j, i, replacement);
				}
			}
		}
	}
	void opCall() {
		initialize();
		bool success = jjTilesFromTileset("AlienPalace.j2t", 70, 30, makeMapping());
		if (!success) {
			jjAlert("|Failed to load AlienPalace.j2t");
			jjAlert("|The level will not work correctly.");
			return;
		}
		generateCaveSpikes();
		generateCorners();
		placeSpikes();
		placeCorners();
	}
}
Bubble bubble;
Launcher launcher;
LauncherSwitch launcherSwitch;
SpikeBall spikeBall;
Poster poster;
se::DefaultWeaponHook weaponHook;
array<int> gunJammedTimer(jjLocalPlayerCount);
jjPAL palette8;
jjPAL palette16;
int colorDepth = 16;
bool finished = false;
int iabs(int x) {
	return x < 0 ? -x : x;
}
int isign(int x) {
	return x < 0 ? -1 : x > 0 ? 1 : 0;
}
jjPIXELMAP@ makeRopeSprite() {
	jjPIXELMAP tile(5);
	jjPIXELMAP result(5, 8);
	for (int i = 0; i < 8; i++) {
		for (int j = 0; j < 5; j++) {
			result[j, i] = tile[i, 13 + j];
		}
	}
	return result;
}
void pingPongAnimation(const jjANIMATION@ src, jjANIMATION@ dest) {
	for (uint i = 0; i < src.frameCount; i++) {
		jjAnimFrames[dest + i] = jjAnimFrames[src + i];
	}
	for (uint i = 1; i < src.frameCount - 1; i++) {
		jjAnimFrames[dest + dest.frameCount - i] = jjAnimFrames[src + i];
	}
}
jjPIXELMAP@ flipXY(jjPIXELMAP@ image) {
	int width = image.width;
	int height = image.height;
	for (int i = 0; i < height; i++) {
		for (int j = i + 1; j < width; j++) {
			auto t = image[j, i];
			image[j, i] = image[i, j];
			image[i, j] = t;
		}
	}
	return image;
}
jjMASKMAP@ flipXY(jjMASKMAP@ mask) {
	for (int i = 0; i < 32; i++) {
		for (int j = i + 1; j < 32; j++) {
			auto t = mask[j, i];
			mask[j, i] = mask[i, j];
			mask[i, j] = t;
		}
	}
	return mask;
}
int colorDistanceSqr(jjPALCOLOR first, jjPALCOLOR second) {
	int rs = first.red + second.red;
	int r = first.red - second.red;
	int g = first.green - second.green;
	int b = first.blue - second.blue;
	return (1024 + rs) * r * r + 2048 * g * g + (1534 - rs) * b * b;
}
int nearestColor(const jjPAL& palette, jjPALCOLOR color, int begin, int end) {
	int result = begin;
	int bestDist = 1e9f;
	for (int i = begin; i < end; i++) {
		int dist = colorDistanceSqr(palette.color[i], color);
		if (dist < bestDist) {
			result = i;
			bestDist = dist;
		}
	}
	return result;
}
void loadBubbleSprite() {
	auto@ animSet = jjAnimSets[ANIM::CUSTOM[anim_bubble]];
	animSet.allocate(array<uint> = {1 + TrueColor::NumberOfFramesPerImage});
	auto@ animation = jjAnimations[animSet];
	auto@ frame = jjAnimFrames[animation];
	TrueColor::Bitmap image("SEwaterbubble.png");
	int width = image.width;
	int height = image.height;
	frame.hotSpotX = -width / 2;
	frame.hotSpotY = -height / 2;
	image.saveToAnimFrames(animation + 1, TrueColor::Coordinates(0, 0, width, height, frame.hotSpotX, frame.hotSpotY));
	jjPIXELMAP reduced(width, height);
	for (int i = 0; i < height; i++) {
		for (int j = 0; j < width; j++) {
			const auto@ color = image.pixels[j][i];
			if (color.alpha > 0) {
				jjPALCOLOR c(color.red * color.alpha / 255, color.green * color.alpha / 255, color.blue * color.alpha / 255);
				reduced[j, i] = nearestColor(palette8, c, 144, 176);
			}
		}
	}
	reduced.save(frame);
}
jjPALCOLOR darken(jjPALCOLOR color) {
	color.red = int(color.red * 0.8f);
	color.green = int(color.green * 0.85f);
	color.blue = int(color.blue * 0.9f);
	return color;
}
void modifyPalette() {
	palette16 = jjBackupPalette;
	palette16.gradient(jjPALCOLOR(0, 0, 0), jjPALCOLOR(255, 84, 209), 176, 16);
	palette16.gradient(jjPALCOLOR(255, 84, 209), jjPALCOLOR(0, 0, 0), 176 + 16, 16);
	palette16.gradient(jjPALCOLOR(0, 0, 0), jjPALCOLOR(4, 28, 20), 144, 8);
	palette16.gradient(jjPALCOLOR(4, 28, 20), jjPALCOLOR(71, 255, 191), 144 + 8, 24);
	palette16.color[208] = jjPALCOLOR(0, 0, 0);
	palette16.color[209] = jjPALCOLOR(0, 12, 20);
	palette16.color[210] = jjPALCOLOR(0, 20, 33);
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 8; j++) {
			palette16.color[96 + i * 8 + j] = darken(jjBackupPalette.color[32 + i * 16 + j]);
		}
	}
	palette16.color[128] = darken(jjBackupPalette.color[15]);
	palette16.color[129] = darken(jjBackupPalette.color[112]);
	palette8 = palette16;
	palette8.gradient(jjPALCOLOR(45, 152, 210), jjPALCOLOR(8, 71, 106), 144, 8);
	palette8.gradient(jjPALCOLOR(8, 71, 106), jjPALCOLOR(0, 0, 0), 152, 12);
	palette8.gradient(jjPALCOLOR(255, 255, 255), jjPALCOLOR(78, 126, 153), 164, 12);
	for (int i = 0; i < 32; i++) {
		palette8.color[176 + i] = palette16.color[175 - i];
	}
	palette16.apply();
	TrueColor::ProcessPalette();
}
void modifyTiles() {
	flipXY(jjMASKMAP(92)).save(92);
	flipXY(jjPIXELMAP(105)).save(115);
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 6; j++) {
			int id = 164 + i * 10 + j;
			jjPIXELMAP tile(id);
			for (int k = 0; k < 32; k++) {
				for (int m = 0; m < 32; m++) {
					if (tile[m, k] != 0)
						tile[m, k] += 172;
				}
			}
			tile.save(id, true);
		}
	}
	for (int i = 0; i < 8; i++) {
		for (int j = 0; j < 8; j++) {
			int id = 202 + i * 10 + j;
			jjPIXELMAP tile(id);
			for (int k = 0; k < 32; k++) {
				for (int m = 0; m < 32; m++) {
					tile[m, k] -= 32;
				}
			}
			tile.save(id);
		}
	}
	array<int> layer4Tiles;
	int width = jjLayerWidth[4];
	int height = jjLayerHeight[4];
	for (int i = 0; i < height; i++) {
		for (int j = 0; j < width; j++) {
			int tile = jjTileGet(4, j, i);
			int index = layer4Tiles.find(tile);
			if (index < 0)
				layer4Tiles.insertLast(tile);
		}
	}
	int count = layer4Tiles.length();
	for (int i = 0; i < count; i++) {
		int tile = layer4Tiles[i];
		jjPIXELMAP image(tile);
		int w = image.width;
		int h = image.height;
		for (int j = 0; j < h; j++) {
			for (int k = 0; k < w; k++) {
				int color = image[k, j];
				if (color == 0)
					continue;
				int recolor = color;
				if (color == 15) {
					recolor = 128;
				} else if (color == 112) {
					recolor = 129;
				} else if (color >= 32 && color < 96 && color & 8 == 0) {
					recolor = 96 + (color - 32) / 16 * 8 + color % 8;
				}
				image[k, j] = recolor;
			}
		}
		image.save(tile);
	}
}
void modifyLayers() {
	jjLayers[2].spriteMode = SPRITE::BLEND_DODGE;
	jjLayers[2].spriteParam = 224;
	jjLayers[3].spriteParam = 255;
	jjLayers[8].textureSurface = SURFACE::FULLSCREEN;
	for (int i = 5; i <= 7; i++) {
		jjLayers[i].xSpeedModel = LAYERSPEEDMODEL::FROMSTART;
		jjLayers[i].ySpeedModel = LAYERSPEEDMODEL::FROMSTART;
	}
	jjLAYER aurora(jjLayers[8]);
	aurora.texture = TEXTURE::MEZ02;
	aurora.spriteMode = SPRITE::BLEND_DODGE;
	aurora.spriteParam = 64;
	aurora.xAutoSpeed *= -0.89f;
	aurora.yAutoSpeed *= -0.86f;
	aurora.warpHorizon.stars = false;
	aurora.warpHorizon.fadePositionY = 0.7f;
	jjLAYER ocean(8, 8);
	ocean.texture = TEXTURE::DIAMONDUSBETA;
	ocean.textureSurface = SURFACE::FULLSCREEN;
	ocean.textureStyle = TEXTURE::REFLECTION;
	ocean.reflection.distortion = 96;
	ocean.reflection.top = 0.7f;
	ocean.reflection.tintColor = 210;
	ocean.reflection.tintOpacity = 64;
	ocean.xAutoSpeed = 0.07f;
	ocean.yAutoSpeed = 0.02f;
	auto@ order = jjLayerOrderGet();
	order.removeAt(1);
	order.insertAt(2, jjLayers[2]);
	order.insertAt(order.length() - 2, aurora);
	order.insertAt(order.length() - 4, ocean);
	jjLayerOrderSet(order);
	jjUseLayer8Speeds = true;
	jjWaterLayer = 0;
}
void loadResources() {
	se::roller.loadAnims(jjAnimSets[ANIM::CUSTOM[anim_roller]]);
	se::roller.loadSamples(array<SOUND::Sample> = {sound::roller});
	se::roller.setAsWeapon(9, weaponHook);
	int src = jjAnimSets[ANIM::CUSTOM[anim_temp_pickups]].load(0, "SExmas.j2a");
	int dest = jjAnimSets[ANIM::PICKUPS];
	for (int i = 0; i < 95; i++) {
		const jjANIMATION@ anim = jjAnimations[src + i];
		if (anim.frameCount != 0)
			jjAnimations[dest + i] = anim;
	}
	loadBubbleSprite();
	jjAnimSets[ANIM::CUSTOM[anim_temp_penguin]].load(11, "HH18E1.j2a");
	jjAnimSets[ANIM::CUSTOM[anim_rope]].allocate(array<uint> = {1});
	jjAnimSets[ANIM::CUSTOM[anim_launcher]].load(0, "SEhh24.j2a");
	jjAnimSets[ANIM::CUSTOM[anim_spike_ball]].load(1, "SEhh24.j2a");
	jjAnimSets[ANIM::CUSTOM[anim_poster]].load(2, "SEhh24.j2a");
	jjAnimSets[ANIM::CUSTOM[anim_penguin]].allocate(array<uint> = {jjAnimations[jjAnimSets[ANIM::CUSTOM[anim_temp_penguin]]].frameCount * 2 - 2});
	pingPongAnimation(jjAnimations[jjAnimSets[ANIM::CUSTOM[anim_temp_penguin]]], jjAnimations[jjAnimSets[ANIM::CUSTOM[anim_penguin]]]);
	auto@ ropeAnimFrame = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::CUSTOM[anim_rope]]]];
	makeRopeSprite().save(ropeAnimFrame);
	ropeAnimFrame.hotSpotX = -ropeAnimFrame.width / 2;
	jjSampleLoad(sound::spikeballHit, "SEhh24spikeball1.wav");
	jjSampleLoad(sound::spikeballRecover, "SEhh24spikeball2.wav");
	jjSampleLoad(sound::launcherSwitch, "SEhh24launcherswitch.wav");
	jjSampleLoad(sound::dryFire, "SEhh24dryfire.wav");
	jjSampleLoad(sound::secret, "SEhh24secret.wav");
	jjSampleLoad(sound::launcherHit, "SEhh24launcher1.wav");
	jjSampleLoad(sound::launcherFire, "SEhh24launcher2.wav");
}
void setBehaviors() {
	jjObjectPresets[event_deleter].behavior = BEHAVIOR::INACTIVE;
	jjObjectPresets[event_floor_spike].behavior = BEHAVIOR::INACTIVE;
	jjObjectPresets[event_ceiling_spike].behavior = BEHAVIOR::INACTIVE;
	jjObjectPresets[OBJECT::BUBBLE].behavior = BEHAVIOR::INACTIVE;
	jjObjectPresets[OBJECT::LIZARD].determineCurAnim(ANIM::CUSTOM[anim_penguin], 0);
	launcher.assign(jjObjectPresets[event_launcher]);
	spikeBall.assign(jjObjectPresets[OBJECT::SPIKEBOLL]);
	poster.assign(jjObjectPresets[OBJECT::CHESHIRE1]);
	SMOKE::FROZENALIEN(OBJECT::DRAGON, 3);
	int id = jjAddObject(0, 0.f, 0.f);
	if (id > 0) {
		auto@ obj = jjObjects[id];
		obj.deactivates = false;
		obj.behavior = function(obj) {
			jjSetWaterLevel(jjLayerHeight[4] * 32 + 128.f, true);
		};
	}
}
void deleteUnwantedEvents() {
	int width = jjLayerWidth[4];
	int height = jjLayerHeight[4] - 1;
	for (int i = 0; i < height; i++) {
		for (int j = 0; j < width; j++) {
			int event = jjEventGet(j, i);
			if (event == event_deleter)
				jjParameterSet(j, i + 1, -12, 32, 0);
		}
	}
}
void overwriteSpikes() {
	int width = jjLayerWidth[4];
	int height = jjLayerHeight[4];
	for (int i = 0; i < height; i++) {
		for (int j = 0; j < width; j++) {
			if (jjEventGet(j, i) == AREA::HURT)
				jjEventSet(j, i, jjParameterGet(j, i, 1, 1) == 0 ? event_floor_spike : event_ceiling_spike);
		}
	}
}
void updateVideoSettings() {
	for (int i = 1; i <= 3; i++) {
		jjLayers[8 - i].yOffset = -i * (jjSubscreenHeight - 450) / 4;
	}
	if (jjColorDepth != colorDepth) {
		colorDepth = jjColorDepth;
		auto@ order = jjLayerOrderGet();
		order[order.length() - 1].texture = colorDepth == 8 ? TEXTURE::MEZ02 : TEXTURE::FROMTILES;
		order[order.length() - 3].spriteMode = colorDepth == 8 ? SPRITE::TRANSLUCENT : SPRITE::BLEND_DODGE;
		order[order.length() - 5].reflection.tintOpacity = colorDepth == 8 ? 128 : 64;
		(jjColorDepth == 8 ? palette8 : palette16).apply();
	}
}
bool touchesSpikes(jjPLAYER@ player) {
	if (player.noclipMode)
		return false;
	for (int i = 0; i < 2; i++) {
		if (i == 0 ? player.ySpeed < 0.f : player.ySpeed > 0.f)
			continue;
		int x = int(player.xPos) - 12;
		int y = int(player.yPos) + (i == 0 ? 21 : -12);
		int w = 24;
		int event = i == 0 ? event_floor_spike : event_ceiling_spike;
		if (jjMaskedHLine(x, w, y)) {
			if (int(jjEventAtLastMaskedPixel) == event)
				return true;
			if (x & 31 > 8) {
				w -= ~x & 31;
				x += ~x & 31;
				if (jjMaskedHLine(x, w, y) && int(jjEventAtLastMaskedPixel) == event)
					return true;
			}
		}
	}
	return false;
}
jjOBJ@ getBubble(const jjPLAYER@ player) {
	for (int i = 0; i < jjObjectCount; i++) {
		const auto@ obj = jjObjects[i];
		if (cast<const jjBEHAVIORINTERFACE>(obj.behavior) is bubble) {
			float x = obj.xPos - player.xPos;
			float y = obj.yPos - player.yPos;
			if (x * x + y * y < obj.var[bubble_size] * obj.var[bubble_size])
				return obj;
		}
	}
	return null;
}
bool onDrawAmmo(jjPLAYER@ player, jjCANVAS@ canvas) {
	return weaponHook.drawAmmo(player, canvas);
}
bool onDrawScore(jjPLAYER@ player, jjCANVAS@ canvas) {
	HH24::score(player, canvas, false);
	return false;
}
void onLevelLoad() {
	deleteUnwantedEvents();
	AlienPalaceSpikeImporter()();
	modifyPalette();
	modifyTiles();
	modifyLayers();
	loadResources();
	setBehaviors();
	overwriteSpikes();
	updateVideoSettings();
	HH24::levelLoad();
}
void onLevelReload() {
	(jjColorDepth == 8 ? palette8 : palette16).apply();
	HH24::levelReload();
}
void onMain() {
	updateVideoSettings();
	HH24::main();
	const auto@ frontLayer = jjLayers[3];
	const jjPLAYER@ hiddenPlayer = null;
	for (int i = 0; i < jjLocalPlayerCount; i++) {
		auto@ player = jjLocalPlayers[i];
		int x = int(player.xPos) / 32;
		int y = int(player.yPos) / 32;
		if (x >= 0 && x < frontLayer.width && y >= 0 && y < frontLayer.height && frontLayer.tileGet(x, y) != 0) {
			@hiddenPlayer = player;
			break;
		}
	}
	if (hiddenPlayer !is null) {
		frontLayer.spriteMode = SPRITE::BLEND_NORMAL;
		if (frontLayer.spriteParam == 255)
			jjSamplePriority(sound::secret);
		if (frontLayer.spriteParam > 45)
			frontLayer.spriteParam -= 10;
	} else {
		if (frontLayer.spriteParam < 255)
			frontLayer.spriteParam += 10;
		else
			frontLayer.spriteMode = SPRITE::NORMAL;
	}
	array<jjOBJ@> bubbles;
	array<jjOBJ@> spikeBalls;
	for (int i = 0; i < jjObjectCount; i++) {
		auto@ obj = jjObjects[i];
		if (obj.isActive) {
			const auto@ behavior = cast<const jjBEHAVIORINTERFACE>(obj.behavior);
			if (behavior is bubble)
				bubbles.insertLast(@obj);
			else if (behavior is spikeBall && obj.state == STATE::ATTACK)
				spikeBalls.insertLast(@obj);
		}
	}
	for (uint i = 0; i < bubbles.length(); i++) {
		auto@ obj = bubbles[i];
		int radius = obj.var[bubble_size];
		for (uint j = 0; j < spikeBalls.length(); j++) {
			auto@ ball = spikeBalls[j];
			float x = ball.xPos - obj.xPos;
			float y = ball.yPos - obj.yPos;
			if (x * x + y * y < radius * radius)
				bubble.destroy(obj);
		}
	}
}
void onPlayer(jjPLAYER@ player) {
	HH24::player(player);
	if (touchesSpikes(player))
		player.hurt();
	const auto@ bubble = getBubble(player);
	jjSetWaterLevel(bubble !is null ? 0.f : jjLayerHeight[4] * 32 + 128.f, true);
	auto event = jjEventGet(int(player.xPos) / 32, int(player.yPos) / 32);
	if (event == AREA::EOL) {
		finished = true;
		HH24::gem::saveGemData();
	}
	if (finished && bubble !is null) {
		const float speed = 0.5f;
		player.xSpeed = 0.f;
		player.ySpeed = 0.f;
		player.xPos += bubble.xSpeed;
		player.yPos += bubble.ySpeed;
		float dx = bubble.xPos - player.xPos;
		float dy = bubble.yPos - player.yPos;
		float d = sqrt(dx * dx + dy * dy);
		if (d > speed) {
			dx *= speed / d;
			dy *= speed / d;
		}
		player.xPos += dx;
		player.yPos += dy;
		if (jjEventGet(int(bubble.xPos) / 32, int(bubble.yPos) / 32) == AREA::FLYOFF) {
			bubble.xSpeed = 0.f;
			bubble.ySpeed = 0.f;
		}
	}
}
void onPlayerInput(jjPLAYER@ player) {
	if (!player.keyFire || player.frozen > 0) {
		gunJammedTimer[player.localPlayerID] = 0;
		return;
	}
	if (getBubble(player) !is null) {
		if (gunJammedTimer[player.localPlayerID] == 0) {
			jjSamplePriority(sound::dryFire);
			const auto@ frame = jjAnimFrames[player.curFrame];
			float x = player.xPos + (player.direction < 0 ? -1 : 1) * (frame.hotSpotX - frame.gunSpotX);
			float y = player.yPos + frame.hotSpotY - frame.gunSpotY;
			int id = jjAddObject(OBJECT::EXPLOSION, x, y);
			if (id > 0)
				jjObjects[id].determineCurAnim(ANIM::AMMO, 6);
		}
		player.keyFire = false;
		gunJammedTimer[player.localPlayerID] = 30;
		return;
	}
	if (gunJammedTimer[player.localPlayerID] > 0) {
		player.keyFire = false;
		gunJammedTimer[player.localPlayerID]--;
		return;
	}
	gunJammedTimer[player.localPlayerID] = -1;
}