Downloads containing ab23btl08.j2as

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

File preview

const bool MLLESetupSuccessful = MLLE::Setup(array<MLLEWeaponApply@> = {null, null, BubbleGun::Weapon(), null, null, null, WeaponVMega::GravityWell::Weapon(), null, null}); ///@MLLE-Generated
#include "MLLE-Include-1.5w.asc" ///@MLLE-Generated
#pragma require "ab23btl08-MLLE-Data-4.j2l" ///@MLLE-Generated
#pragma require "ab23btl08-MLLE-Data-3.j2l" ///@MLLE-Generated
#pragma require "ab23btl08-MLLE-Data-2.j2l" ///@MLLE-Generated
#pragma require "ab23btl08-MLLE-Data-1.j2l" ///@MLLE-Generated
#pragma require "ab23btl08.j2l" ///@MLLE-Generated
#include "WeaponVMega7.asc" ///@MLLE-Generated
#pragma require "WeaponVMega7.asc" ///@MLLE-Generated
#include "BubbleGun.asc" ///@MLLE-Generated
#pragma require "BubbleGun.asc" ///@MLLE-Generated
#pragma require "spelunky_ignite.wav"
#pragma require "Loonejjjjxdddd3.pal"

array<bool> canJump(jjLocalPlayerCount, true);
array<bool> canJumpTwo(jjLocalPlayerCount, true);

jjPAL PsychPal;

bool palCycle = true;
const string filename = "malice.asdat";

void onLevelLoad() {
	PsychPal.load("Loonejjjjxdddd3.pal");
	PsychPal.color[255].red = PsychPal.color[255].green = PsychPal.color[255].blue = 0;
	PsychPal.apply();
	
	jjObjectPresets[OBJECT::BRIDGE].behavior = MyBridge;
	
	jjLayers[1].spriteMode = SPRITE::BLEND_NORMAL;
	jjLayers[1].spriteParam = 255;
	
	jjTexturedBGFadePositionY = 0.30;
	
	jjSampleLoad(SOUND::P2_POEP, "spelunky_ignite.wav");
	
	jjTexturedBGTexture = TEXTURE::PSYCH;
	jjUseLayer8Speeds = true;
	
	jjWaterLayer = 28;
	jjSetWaterGradient(0,22,44, 0,44,22);
	jjWaterLighting = (WATERLIGHT::GLOBAL);
	
	generateCustomSpringSprites(jjAnimSets[ANIM::CUSTOM[9]], array<uint> = {40, 16, 64, 88});
    turnIntoCustomSpring(jjObjectPresets[OBJECT::FROZENSPRING], 0, 19.75f, false);
    turnIntoCustomSpring(jjObjectPresets[OBJECT::HORREDSPRING], 1, 26.55f, false);
    turnIntoCustomSpring(jjObjectPresets[OBJECT::HORGREENSPRING], 2, 17.75f, false);
    turnIntoCustomSpring(jjObjectPresets[OBJECT::HORBLUESPRING], 3, 33.3f, false);
	
	jjObjectPresets[OBJECT::HORREDSPRING].causesRicochet = jjObjectPresets[OBJECT::HORBLUESPRING].causesRicochet = jjObjectPresets[OBJECT::HORGREENSPRING].causesRicochet = false;

    jjObjectPresets[OBJECT::GUNCRATE].behavior = ColoredCrate();
	jjObjectPresets[OBJECT::SHARD].behavior = ColoredCrate();
	
	// jjDelayGeneratedCrateOrigins = true;
	
	jjWeapons[WEAPON::GUN8].spread = SPREAD::NORMAL;
	jjObjectPresets[OBJECT::FIREBALLBULLETPU].var[6] = 8 + 16;
	
	jjObjectPresets[OBJECT::BOUNCERPOWERUP].direction = -1;
	jjObjectPresets[OBJECT::GUN9POWERUP].direction = -1;
	jjObjectPresets[OBJECT::TOASTERPOWERUP].direction = 0;
	
	jjObjectPresets[OBJECT::CARROT].direction = SPRITE::FLIPV;
	jjObjectPresets[OBJECT::ICEPOWERUP].direction = SPRITE::FLIPV;
	// jjObjectPresets[OBJECT::ICEAMMO15].direction = SPRITE::FLIPH;
	jjObjectPresets[OBJECT::SMALLTREE].direction = SPRITE::FLIPV;
	
	jjObjectPresets[OBJECT::SWINGINGVINE].behavior = SyncedVine;
	
	jjANIMATION@ anim = jjAnimations[jjAnimSets[ANIM::BRIDGE] + 2];
	for (uint j = 0; j < anim.frameCount; j++) {
		jjANIMFRAME@ frame = jjAnimFrames[anim + j];
		jjPIXELMAP sprite(frame);
		for (uint x = 0; x < sprite.width; ++x) {
			for (uint y = 0; y < sprite.height; ++y) {
				if (sprite[x,y] >= 16 && sprite[x,y] <= 23) sprite[x,y] = 128 + (sprite[x,y]&7)*2;
			}
		}
		sprite.save(frame);
	}
	
	
	jjSTREAM savedSettings(filename);
	while (!savedSettings.isEmpty()) {
		savedSettings.pop(palCycle);
	}
	
}

void onLevelBegin() {
	for (int i = 1; i < jjObjectCount; i++) {
		if (jjObjects[i].eventID == OBJECT::BRIDGE){
			jjObjects[i].yOrg += 7;
			jjObjects[i].yPos += 7;
		}
	}
	for (int x = 0; x < jjLayerWidth[4]; x++) {
        for (int y = 0; y < jjLayerHeight[4]; y++) {
            if ((jjEventGet(x, y) == OBJECT::GENERATOR && jjParameterGet(x, y, 0, 8) == 255) || jjEventGet(x, y) == 255) {
                jjEventSet(x, y, AREA::ONEWAY);
            }
        }
    }
	
		
/*	jjObjectPresets[OBJECT::TOASTERBULLETPU].lightType = LIGHT::BRIGHT;
	jjObjectPresets[OBJECT::TOASTERBULLETPU].light = 8;
	
	jjObjectPresets[OBJECT::FIREBALLBULLETPU].lightType = LIGHT::BRIGHT;
	jjObjectPresets[OBJECT::FIREBALLBULLETPU].light = 8;
	
	jjObjectPresets[OBJECT::RFBULLET].lightType = LIGHT::BRIGHT;
	jjObjectPresets[OBJECT::RFBULLET].light = 8;
	
	jjObjectPresets[OBJECT::BOUNCERBULLETPU].lightType = LIGHT::BRIGHT;
	jjObjectPresets[OBJECT::BOUNCERBULLETPU].light = 8;
	
	jjObjectPresets[OBJECT::ICEBULLETPU].lightType = LIGHT::BRIGHT;
	jjObjectPresets[OBJECT::ICEBULLETPU].light = 8;
	
	jjObjectPresets[OBJECT::ELECTROBULLETPU].lightType = LIGHT::BRIGHT;
	jjObjectPresets[OBJECT::ELECTROBULLETPU].light = 8;
	
	jjObjectPresets[OBJECT::SEEKERBULLETPU].lightType = LIGHT::BRIGHT;
	jjObjectPresets[OBJECT::SEEKERBULLETPU].light = 8; */
}

void onMain() {	
	jjWeapons[WEAPON::GUN8].comesFromGunCrates = true;
    jjWeapons[WEAPON::GUN9].comesFromGunCrates = true;
	
	for (int i = 1; i < jjObjectCount; i++) {
        jjOBJ@ obj = jjObjects[i];
        if (obj.eventID == OBJECT::ICEBULLETPU && obj.xSpeed > -0.5 && obj.xSpeed < 0.5) {
            obj.xSpeed = 0;
        }
		if (jjObjects[i].eventID == OBJECT::CHESHIRE1){ 
			jjObjects[i].yPos = jjObjects[i].yOrg - 15;
			if (jjObjects[i].xPos>118*32 && jjObjects[i].yPos>48*32 && jjObjects[i].xPos<122*32 && jjObjects[i].yPos<52) {
			jjObjectPresets[OBJECT::CHESHIRE1].direction = SPRITE::FLIPHV;
			
			}
		}
		if (jjObjects[i].eventID == OBJECT::BOUNCERPOWERUP) {
			jjObjects[i].yPos = 58.5*32;
			jjObjects[i].ySpeed = 0;
		}
		if (jjObjects[i].eventID == OBJECT::BLASTERPOWERUP) {
			jjObjects[i].yPos = 109.68*32;
			jjObjects[i].ySpeed = 0;
		}
		if (obj.eventID == OBJECT::TOASTERPOWERUP){
			obj.xPos = obj.xOrg + 16;
		}
    }
	
	if (jjGameTicks % 4 == 0 && palCycle) {
        for (int i = 128; i <= 143; i++) {
            jjPalette.color[i].setHSL(PsychPal.color[i].getHue() + jjGameTicks / 6, PsychPal.color[i].getSat(), PsychPal.color[i].getLight());
        }
        jjPalette.apply();
    }
	
	if (jjGameTicks == 210)
		jjConsole("|You can use the following command to pause the color changing foliage:||!palette");


}

void onPlayer(jjPLAYER@ player){
	player.lightType = LIGHT::NONE;
	
	handleFastCustomSpringSpeeds(player);
	
	if (player.yPos >= 0) {
		//Wherein "skill 3" means "MP Only"
		int skill = jjParameterGet(uint16(player.xPos/32), uint16(player.yPos/32), -4, 2);	
		if (skill == 2) {
			if (jjLayers[1].spriteParam > 96) {
				if (jjLayers[1].spriteParam == 255) jjSamplePriority(SOUND::P2_POEP);
				jjLayers[1].spriteParam = jjLayers[1].spriteParam - 10;
			}
		}
		else if (jjLayers[1].spriteParam != 255) {
			jjLayers[1].spriteParam = jjLayers[1].spriteParam + 10;
		}
	}
	
	for (int i = 1; i < jjObjectCount; i++) {
        if (jjObjects[i].isActive && jjObjects[i].eventID == OBJECT::COPTER && jjObjects[i].state == STATE::FLY) {
            //Only set the counter if it's available to take
            jjObjects[i].counter = 0;
            if (jjObjects[i].var[4] == 0)
                jjObjects[i].state = STATE::DONE;
        }
	}
	
	if (!canJump[player.localPlayerID]) {
        player.keyJump = player.keyDown = player.keyFire = player.keyLeft = player.keyRight = false;
    }
	
	if (!canJumpTwo[player.localPlayerID]) {
        player.keyJump = player.keyDown = player.keyFire = false;
    }
	
	if (player.health == 0 || jjGameTicks == 1){
	  canJump[player.localPlayerID] = true;
	  canJumpTwo[player.localPlayerID] = true;
	 // player.yOrg += 32;
	}
}


jjANIMSET@ customSpringSprite;
array<int> fastCustomSpringSpeeds(jjLocalPlayerCount);
bool generateCustomSpringSprites(jjANIMSET@ anim, const array<uint> &in colors) {
    int length = colors.length();
    bool success = (@customSpringSprite = anim).allocate(array<uint>(length * 3, 5)) !is null;
    if (success) {
        uint srcSet = jjAnimSets[ANIM::SPRING];
        for (int i = 0; i < length; i++) {
            uint color = colors[i];
            uint destAnimOffset = anim + i * 3;
            for (int j = 0; j < 3; j++) {
                uint srcAnim = jjAnimations[srcSet + j];
                uint destAnim = jjAnimations[destAnimOffset + j];
                for (int k = 0; k < 5; 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 >= 32 && pixel < 40)
                                image[m, l] = color + (pixel & 7);
                        }
                    }
                    if (!image.save(jjAnimFrames[destAnim + k]))
                        return false;
                }
            }
        }
    }
    return success;
}
void initializeCustomSpring(jjOBJ@ obj) {
    int anim = obj.curAnim;
    obj.behave(obj.behavior = BEHAVIOR::SPRING, false);
    if (obj.curAnim != anim) {
        obj.curAnim = anim + 2;
        obj.determineCurFrame();
    }
    obj.draw();
}
 
void turnIntoCustomSpring(jjOBJ@ obj, uint color, float power, bool horizontal) {
    if (horizontal) {
        obj.xSpeed = power;
        obj.ySpeed = 0.f;
    } else {
        obj.xSpeed = 0.f;
        obj.ySpeed = -power;
        if (obj.state == STATE::START && obj.creatorType == CREATOR::LEVEL) {
            int x = int(obj.xPos) >> 5;
            int y = int(obj.yPos) >> 5;
            if (jjParameterGet(x, y, 0, 1) != 0) {
                jjParameterSet(x, y, 0, 1, 0);
                obj.yPos -= 4.f;
                obj.ySpeed = power;
            }
        }
    }
    obj.behavior = initializeCustomSpring;
    obj.curAnim = customSpringSprite + color * 3 + (horizontal ? 1 : 0);
    obj.energy = obj.frameID = obj.freeze = obj.justHit = obj.light = obj.points = 0;
    obj.isBlastable = obj.isTarget = obj.scriptedCollisions = obj.triggersTNT = false;
    obj.deactivates = obj.isFreezable = true;
    obj.bulletHandling = HANDLING::IGNOREBULLET;
    obj.playerHandling = HANDLING::SPECIAL;
    obj.lightType = LIGHT::NORMAL;
    obj.determineCurFrame();
}
 
void handleFastCustomSpringSpeeds(jjPLAYER@ play) {
        if (play.ySpeed < -32.f) {
                fastCustomSpringSpeeds[play.localPlayerID] = int(ceil((play.ySpeed + 32.f) / -0.125f));
        } else if (fastCustomSpringSpeeds[play.localPlayerID] != 0) {
                if (play.ySpeed < -31.f) {
                        fastCustomSpringSpeeds[play.localPlayerID]--;
                        play.ySpeed = -32.f;
                } else {
                        fastCustomSpringSpeeds[play.localPlayerID] = 0;
                }
        }
}

void onDrawLayer7(jjPLAYER@ play, jjCANVAS@ screen) {
	jjSetWaterLevel((play.cameraY - jjLayers[15].getYPosition(play)) + 320, true);
}
void onDrawLayer4(jjPLAYER@ play, jjCANVAS@ screen) {
  jjSetWaterLevel(16000, true);
}

class ColoredCrate : jjBEHAVIORINTERFACE {
    int color;
    void onBehave(jjOBJ@ obj) {
        obj.behave(obj.eventID == OBJECT::GUNCRATE? BEHAVIOR::CRATE : BEHAVIOR::SHARD);
        
        if (obj.state == STATE::KILL) {
            obj.delete();
        }
    }
    void onDraw(jjOBJ@ obj) {
        color = jjParameterGet(int(obj.xOrg/32), int(obj.yOrg/32)+ (jjEventGet(int(obj.xOrg/32), int(obj.yOrg/32)) == 255? 0:1), 0, 8);
        jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, color == 0? SPRITE::NORMAL : SPRITE::SINGLEHUE, color);
		
    }
}
void onFunction3(jjPLAYER@ play, bool preventJump) {
    canJump[play.localPlayerID] = preventJump;
}

void onFunction4(jjPLAYER@ play, bool preventJump) {
    canJumpTwo[play.localPlayerID] = preventJump;
}

bool onDrawAmmo(jjPLAYER@ player, jjCANVAS@ canvas) {
	return MLLE::WeaponHook.drawAmmo(player, canvas);
}

/* array<string> Areas = {
    "a", "b", "ยง2Loon is so awesome, so is PJ for helping me with this script!", "d", "e", "f", "g", "h"
};
string currArea = Areas[0];

void onFunction0(jjPLAYER@ play, int8 id) {
    currArea = Areas[id];
} 

bool onDrawHealth(jjPLAYER@ play, jjCANVAS@ canvas) {
    canvas.drawString(jjSubscreenWidth - 16 - (currArea.length()*8), 64, currArea, STRING::SMALL, STRING::NORMAL);
    return false;
} */

//violet's witchcraft bridges

enum BridgeVariables { PhysicalWidth, MaximumSagDistance, VisualWidth, FirstObjectIDOfPlatformForSplitscreenPlayers, Angle = FirstObjectIDOfPlatformForSplitscreenPlayers + 3 };
void MyBridge(jjOBJ@ obj) {
//first, check collision with bridge

	if (obj.state==STATE::START) {
		obj.state=STATE::STILL;

		const uint xTile = uint(obj.xOrg) >> 5, yTile = uint(obj.yOrg) >> 5;
		obj.var[BridgeVariables::PhysicalWidth] = 32 * jjParameterGet(xTile,yTile, 0,4);
		obj.curAnim = jjAnimSets[ANIM::BRIDGE].firstAnim + (jjParameterGet(xTile,yTile, 4,3) % 7); //"Type" parameter... % 7 because there are only seven bridge types for some reason.

		int toughness = jjParameterGet(xTile,yTile, 7,4);
		if (toughness == 0) toughness = 4; //default toughness of 4, to avoid dividing by zero
		obj.var[BridgeVariables::MaximumSagDistance] = obj.var[BridgeVariables::PhysicalWidth] / toughness;
		
		//int heightInTiles = jjParameterGet(xTile,yTile, 11,-5);
		int heightInTiles = jjParameterGet(xTile, yTile-1, 0, -8);
		obj.var[BridgeVariables::Angle] = int(atan2(heightInTiles, obj.var[BridgeVariables::PhysicalWidth] / 32) * 162.974662f);
		obj.xAcc = jjCos(obj.var[BridgeVariables::Angle]);
		obj.yAcc = jjSin(obj.var[BridgeVariables::Angle]);
		
		{ //determine how wide the bridge is, in drawn pixels (will always be >= how wide it is in mask pixels)
			int frameID = 0;
			int bridge_len = 0;
			const int numberOfFramesUsedByAnimation = jjAnimations[obj.curAnim].frameCount;
			const uint firstBridgeFrameID = jjAnimations[obj.curAnim].firstFrame;
			while (true) {
				if ((bridge_len += jjAnimFrames[firstBridgeFrameID + frameID].width) >= obj.var[BridgeVariables::PhysicalWidth])
					break;
					
				if (++frameID >= numberOfFramesUsedByAnimation)
					frameID = 0;
			}
			obj.var[BridgeVariables::VisualWidth] = bridge_len;
		}

		obj.xOrg -= 16; //start at left edge of tile, not center
		obj.yOrg -= 6; //worth noting that bridges are never deactivated, so we don't need to worry about where this gets moved to at all
		
		for (int i = 1; i < jjLocalPlayerCount; ++i) { //this portion has no native JJ2 counterpart, because the API for platforms is still pretty limited
			const int platformObjectID = jjAddObject(OBJECT::BRIDGE, 0,0, obj.objectID,CREATOR::OBJECT, BEHAVIOR::BEES);
			jjOBJ@ platform = jjObjects[platformObjectID];
			platform.deactivates = false;
			platform.curFrame = jjAnimations[obj.curAnim].firstFrame;
			obj.var[BridgeVariables::FirstObjectIDOfPlatformForSplitscreenPlayers - 1 + i] = platformObjectID;
		}
	}
	
	obj.clearPlatform();
	for (int i = 1; i < jjLocalPlayerCount; ++i)
		jjObjects[obj.var[BridgeVariables::FirstObjectIDOfPlatformForSplitscreenPlayers - 1 + i]].clearPlatform();

	array<int> pressXPosition;
	array<jjPLAYER@> pressPlayer;
	for (int playerID = 0; playerID < 32; ++playerID) {
		jjPLAYER@ play = jjPlayers[playerID];
		if (play.isActive && play.shieldType != SHIELD::LASER && jjObjects[play.platform].eventID != OBJECT::BURGER) { //all active players are valid, even if isLocal is false
			const int tx = int(play.xPos-obj.xOrg);
			const int ty = int(play.yPos-obj.yOrg - obj.yAcc / obj.xAcc * tx);

			if ((tx >= 0) && (tx <= obj.var[BridgeVariables::PhysicalWidth]) && //player is within bridge area (horizontal)
				(ty > -32) && (ty < obj.var[BridgeVariables::MaximumSagDistance]) && //(and vertical) //-32 was -24
				(play.ySpeed > -1.f)) //not jumping, using a spring, etc.
			{
				pressXPosition.insertLast(tx);
				pressPlayer.insertLast(play);
			}
		}
	}
	
	float max, amp, leftamp, rightamp;
	int	leftmostPressedX, rightmostPressedX;

	if (pressPlayer.length != 0) {
		if (pressPlayer.length > 1) {
			leftmostPressedX=12312312;
			rightmostPressedX=0;
			uint t = 0;
			do {
				const int pressedX = pressXPosition[t];
				if (pressedX < leftmostPressedX) 
					leftmostPressedX = pressedX;
				if (pressedX > rightmostPressedX)
					rightmostPressedX = pressedX;
			} while (++t < pressPlayer.length);

			leftamp =  obj.var[BridgeVariables::MaximumSagDistance]*jjSin((512* leftmostPressedX)/obj.var[BridgeVariables::PhysicalWidth]);
			rightamp = obj.var[BridgeVariables::MaximumSagDistance]*jjSin((512*rightmostPressedX)/obj.var[BridgeVariables::PhysicalWidth]);
		}
		
		uint t = 0;
		uint numberOfLocalPlayersNeedingPlatforms = 0;
		do {
			const auto pressedPosition = pressXPosition[t];
			if (pressPlayer.length == 1)
				max = obj.var[BridgeVariables::MaximumSagDistance] * jjSin((512 * pressedPosition) / obj.var[BridgeVariables::PhysicalWidth]); //same formula as side amps above, but for single player bridges
			else if ((pressedPosition>leftmostPressedX) && (pressedPosition<rightmostPressedX))
				max = leftamp+(rightamp-leftamp)*(pressedPosition-leftmostPressedX)/(rightmostPressedX-leftmostPressedX);
			else
				max = obj.var[BridgeVariables::MaximumSagDistance]*jjSin((512 * pressedPosition)/obj.var[BridgeVariables::PhysicalWidth]);

			jjPLAYER@ play = pressPlayer[t];
			play.yPos = obj.yOrg + obj.yAcc / obj.xAcc * pressedPosition + max - 24;
			if (play.isLocal) {
				jjOBJ@ platform = (numberOfLocalPlayersNeedingPlatforms == 0) ? obj : jjObjects[obj.var[BridgeVariables::FirstObjectIDOfPlatformForSplitscreenPlayers - 1 + numberOfLocalPlayersNeedingPlatforms]];
				platform.bePlatform(
					platform.xPos = play.xPos,
					platform.yPos = play.yPos + 24
				);
				//platform.draw();
				if (play.buttstomp < 120)
					play.buttstomp = 120;
				numberOfLocalPlayersNeedingPlatforms += 1;
			}
		} while (++t < pressPlayer.length);
	}

	//draw
	float bridge_len_x = 0, bridge_len_y = 0;
	int frameID = 0;
	const int numberOfFramesUsedByAnimation = jjAnimations[obj.curAnim].frameCount;
	while (true) {	//cooba - change specifically for mlfingers.j2l conditions
		obj.curFrame = jjAnimations[obj.curAnim].firstFrame + frameID;
		const jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
		
		float plankOffset = 0; //"straight bridge, or terugveren"
		if (pressPlayer.length == 1) {
			const auto pressedPosition = pressXPosition[0];
			plankOffset = ((bridge_len_x<pressedPosition) ?
				(max*jjSin(int(256*bridge_len_x)/pressedPosition)) : //left
				(max*jjCos(int(256*(bridge_len_x-pressedPosition))/(obj.var[BridgeVariables::VisualWidth]-pressedPosition) ))
			);
		} else if (pressPlayer.length > 1) {
			if (bridge_len_x < leftmostPressedX)
				plankOffset = (leftamp*jjSin(int(256*bridge_len_x)/leftmostPressedX));
			else if (bridge_len_x > rightmostPressedX)
				plankOffset = (rightamp*jjCos(int(256*(bridge_len_x-rightmostPressedX))/(obj.var[BridgeVariables::VisualWidth]-rightmostPressedX) ));
			else
				plankOffset = leftamp+(rightamp-leftamp)*(bridge_len_x-leftmostPressedX)/(rightmostPressedX-leftmostPressedX);
		}
		jjDrawRotatedSpriteFromCurFrame(
			obj.xOrg + bridge_len_x - frame.hotSpotX,
			obj.yOrg + bridge_len_y + plankOffset,
			obj.curFrame,
			-obj.var[BridgeVariables::Angle],
			1,
			1,
			jjLocalPlayers[0].shieldType == SHIELD::LASER? SPRITE::TRANSLUCENTPALSHIFT : SPRITE::NORMAL,
			96
		);

		if (int(bridge_len_x += obj.xAcc * frame.width) >= obj.var[BridgeVariables::PhysicalWidth])
			break;
		bridge_len_y += obj.yAcc * frame.width;
			
		if (++frameID >= numberOfFramesUsedByAnimation)
			frameID = 0;
	}
} 

const uint NumberOfBitsDevotedToSyncParameter = 2; //this should correspond to the number of bits you assign to the Sync parameter in your JCS.ini (or MLLE equivalent) entry for Swinging Vine. So for example if you give it a parameter Sync:2, this variable should ALSO equal 2.

 
void SyncedVine(jjOBJ@ obj) {
  obj.var[1] = 128; //vine length--set this in whatever way appeals to you, but a constant 128 is the value that normal swinging vines use.
 
  if (lastSwingingVineLUTLength != obj.var[1]) { //need to generate LUT (LookUp Table) by doing the same math swinging vine objects do
    lastSwingingVineLUTLength = obj.var[1];
    PossibleVineVariableConfigurations = array<array<int>> = {{obj.var[1] * 256, 0}};
    while (true) {
      const array<int>@ oldConfiguration = @PossibleVineVariableConfigurations[PossibleVineVariableConfigurations.length-1];
      array<int> newConfiguration(2);
      newConfiguration[1] = oldConfiguration[1] + ((oldConfiguration[0] > 0) ? -32 : 32);
      newConfiguration[0] = oldConfiguration[0] + newConfiguration[1];
      if (newConfiguration[1] == 0 && newConfiguration[0] == obj.var[1] * 256) //gone full circle
        break;
      PossibleVineVariableConfigurations.insertLast(newConfiguration);
    }
  }
 
  const array<int>@ syncedConfiguration = PossibleVineVariableConfigurations[(jjGameTicks + (jjParameterGet(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, 0, NumberOfBitsDevotedToSyncParameter) * (PossibleVineVariableConfigurations.length / (1 << NumberOfBitsDevotedToSyncParameter)))) % PossibleVineVariableConfigurations.length];
  for (uint i = 0; i < 2; ++i)
    obj.var[2 + i] = syncedConfiguration[i];
   
  //clean up:
  obj.state = STATE::ACTION;
  obj.behavior = BEHAVIOR::SWINGINGVINE;
  obj.behave();
}
int lastSwingingVineLUTLength = -1;
array<array<int>> PossibleVineVariableConfigurations;

bool onLocalChat(string &in text, CHAT::Type type) {
	if (jjRegexMatch(text, "!palette", true)) {
		if (palCycle) {
			jjConsole("Palette cycling disabled!");
			palCycle = false;
			jjSTREAM stream;
			stream.push(palCycle);
			stream.save(filename);
		}
		else {
			jjConsole("Palette cycling enabled!");
			palCycle = true;
			jjSTREAM stream;
			stream.push(palCycle);
			stream.save(filename);
		}
	return true;
	}
	else return false;
}