Downloads containing violetmaze5.j2as

Downloads
Name Author Game Mode Rating
JJ2 1.23 vanilla: Miscellaneous stuff Violet CLM Multiple N/A Download file

File preview

const uint MAPWIDTH = 8;
const uint MAPHEIGHT = 8;
const uint BLOCKWIDTH = 10;
const uint BLOCKHEIGHT = 8;
const uint AVAILABLEBLOCKROWS = 5;
const uint DEADROWSBETWEENBLOCKSANDMAP = 32;

enum TRISTATE { triFALSE, triTRUE, triMU }

float StartPosX, StartPosY;
uint16 EndPosX, EndPosY; bool BegunCyclingAlready = false; int TimeToCycleForReal = 0x7FFFFFFF;

const float BUBBASPEED = 3;
bool BubbaEnraged = false;
int StoreX = -1, StoreY = -1;

class blockProperties {
	TRISTATE left, right, top, bottom, exit, start, store, pole;
	blockProperties() {
		clear(triMU);
	}
	void clear(TRISTATE ClearTo = triFALSE) {
		left = right = top = bottom = exit = start = store = pole = ClearTo;
	}
	bool allows(const blockProperties@ &in other) {
		if((left   != triMU && left   != other.left  )
		|| (right  != triMU && right  != other.right )
		|| (top    != triMU && top    != other.top   )
		|| (bottom != triMU && bottom != other.bottom)
		|| (start  != triMU && start  != other.start )
		|| (exit   != triMU && exit   != other.exit  )
		|| (store  != triMU && store  != other.store )
		|| (pole   != triMU && pole   != other.pole  ))
			return false;
		return true;
	}

}

void onMain() {
	if (jjGameTicks == 1) { //create everything
		if (jjIsServer || jjGameConnection == GAME::LOCAL) {
			if (jjIsServer) {
				if (jjGameMode != GAME::COOP) { jjChat("/gamemode coop"); jjChat("/autostart on"); jjChat("/r"); return; }
			}
			while (RANDSEED0 == 0) RANDSEED0 = jjRandom();
			RANDSEED0 |= uint(0x80000000);
			for (int i = 0; i < 32; ++i)
				jjTriggers[i] = ((RANDSEED0 & (1 << i)) != 0);
			RANDSEED2 = RANDSEED1 = RANDSEED0;
			jjDebug("Seed: " + RANDSEED0);
			generateLevel();
		}
	} else if (jjGameTicks == 2 && jjGameConnection == GAME::LOCAL) {
		moveLocalPlayersIntoLevel();
	} else {
		if (jjGameConnection != GAME::LOCAL && jjGameMode == GAME::COOP) {
			if (jjIsServer) {
			} else { //jjIsClient
				if (jjTriggers[31]) {
					for (int i = 0; i < 32; ++i)
						if (jjTriggers[i]) RANDSEED0 |= (1 << i);
					jjTriggers[31] = false;
					RANDSEED2 = RANDSEED1 = RANDSEED0;
					jjDebug("Seed: " + RANDSEED0);
					generateLevel();
				}
			}
		}
		if (RANDSEED0 != 0) {
			monitorActiveObjects();
			shouldTheLevelBeCyclingRightAboutNow();
		}
		//if ((jjGameTicks & 63) == 0) jjDebug("" + ++jjPlayers[31].score);
	}
}

void moveLocalPlayersIntoLevel() {
	for (int i = 0; i < jjLocalPlayerCount; ++i) {
		jjPLAYER@ play = jjLocalPlayers[i];
		if (jjGameConnection != GAME::LOCAL) { ++play.lives; play.kill(); }//in order to load the new event map. don't ask me why this works/is needed.
		play.xPos = play.xOrg = StartPosX;
		play.yPos = play.yOrg = StartPosY;
		recentDeath[i] = true;
	}
}
void generateLevel() {
	array<array<blockProperties>> levelBlockRequirements(MAPWIDTH, array<blockProperties>(MAPHEIGHT));

	uint firstX = randRange(0, MAPWIDTH);
	TRISTATE pitLevel = (jjEventGet(jjLayerWidth[4] - 2, jjLayerHeight[4] - 2) == 255) ? triMU : triFALSE; //this feels wrong
	for (uint y = 0; y < MAPHEIGHT; ++y) {
		for (uint x = 0; x < MAPWIDTH; ++x)
			levelBlockRequirements[x][y].store = triFALSE;
		uint nextX = randRange(0, MAPWIDTH);
		if (y == 0)		levelBlockRequirements[firstX][y].start = triTRUE;
		else			levelBlockRequirements[firstX][y].top = triTRUE;
		if (y == MAPHEIGHT-1) {	levelBlockRequirements[nextX][y].exit = triTRUE; for (uint x = 0; x < MAPWIDTH; ++x) levelBlockRequirements[x][y].bottom = pitLevel; }
		else			levelBlockRequirements[nextX][y].bottom = triTRUE;
		while (firstX < nextX) {
			levelBlockRequirements[firstX++][y].right = triTRUE;
			levelBlockRequirements[firstX][y].left = triTRUE;
		}
		while (firstX > nextX) {
			levelBlockRequirements[firstX--][y].left = triTRUE;
			levelBlockRequirements[firstX][y].right = triTRUE;
		}
	}
	
	if (randRange(0, 4) == 0) { //add a store
		for (int i = 0; i < 7; ++i) { //try at most seven times to place a store before giving up
			uint randX = randRange(0, MAPWIDTH);
			uint randY = randRange(0, MAPHEIGHT);
			blockProperties@ block = @levelBlockRequirements[randX][randY];
			if (block.top == triMU && block.bottom == triMU && block.left == triMU && block.right == triMU) { //unused slot
				block.store = triTRUE;
				bool faceRight = ((randX == 0) || ((randX != MAPWIDTH-1) && randRange(0,2) == 1));
				if (faceRight) { block.right = triTRUE; levelBlockRequirements[randX+1][randY].left = triTRUE; } 
				else { block.left = triTRUE; levelBlockRequirements[randX-1][randY].right = triTRUE; }
				StoreX = randX; StoreY = randY;
				break;
			}
		}
	}

	uint availableBlockColumns = jjLayerWidth[4] / BLOCKWIDTH;
	uint availableBlockNumber = availableBlockColumns * AVAILABLEBLOCKROWS;
	array<blockProperties> availableBlocks(availableBlockNumber);
	for (uint y = 0, i = 0; y < AVAILABLEBLOCKROWS; ++y)
		for (uint x = 0; x < availableBlockColumns; ++x)
			analyzeBlock(@availableBlocks[i++], x * BLOCKWIDTH, y * BLOCKHEIGHT);		
	
	uint yOrigin = jjLayerHeight[4] - (BLOCKHEIGHT * MAPHEIGHT);
	for (uint yDest = 0; yDest < MAPHEIGHT; ++yDest)
		for (uint xDest = 0, i = 0; xDest < MAPWIDTH; ++xDest) {
			blockProperties@ block = @levelBlockRequirements[xDest][yDest];
			//if (block.top == triMU && block.bottom == triMU && block.left == triMU && block.right == triMU) continue;
			uint newBlockID = getBlockID(@block, @availableBlocks);
			if (yDest < MAPHEIGHT-1 && availableBlocks[newBlockID].bottom == triFALSE)
				levelBlockRequirements[xDest][yDest+1].pole = triFALSE;
			uint xSrc = newBlockID % availableBlockColumns;
			uint ySrc = newBlockID / availableBlockColumns;
			transplantBlock(@block, xDest * BLOCKWIDTH, yDest * BLOCKHEIGHT + yOrigin, xSrc * BLOCKWIDTH, ySrc * BLOCKHEIGHT);
		}
	for (int xSrc = 0; xSrc < jjLayerWidth[4]; ++xSrc)
		setTopColumn(xSrc, jjTileGet(4, xSrc, yOrigin));
	for (int yDest = BLOCKHEIGHT * AVAILABLEBLOCKROWS; yDest < jjLayerHeight[4]; ++yDest)
		for (int xDest = 0; xDest < jjLayerWidth[4]; ++xDest)
			if (jjTileGet(4, xDest, yDest) == WALLSINGLETON)
				jjTileSet(4, xDest, yDest, replaceWallTileAt(4, xDest, yDest));
				
	if (jjGameConnection != GAME::LOCAL)
		moveLocalPlayersIntoLevel();
		
	for(int i=1; i<jjObjectCount; ++i) {
		jjOBJ@ obj = jjObjects[i];
		if(obj.isActive && !isEventASceneryBlock(obj.eventID) && obj.yOrg <= ((BLOCKHEIGHT * AVAILABLEBLOCKROWS + 6) * 32)) jjDeleteObject(i);
	}
	for (int ySrc = 0; ySrc <= (BLOCKHEIGHT * AVAILABLEBLOCKROWS + 6); ++ySrc)
		for (int xSrc = 0, i = 0; xSrc < jjLayerWidth[4]; ++xSrc)
			jjEventSet(xSrc, ySrc, 0);
}

void analyzeBlock(blockProperties@ Block, uint XTile, uint YTile) {
	Block.clear(triFALSE);
	for (uint x = 0; x < BLOCKWIDTH; ++x) {
		if (jjTileGet(4, XTile + x, YTile) == 0)
			Block.top = triTRUE;
		if (jjTileGet(4, XTile + x, YTile + BLOCKHEIGHT - 1) == 0)
			Block.bottom = triTRUE;
	}
	for (uint y = 0; y < BLOCKHEIGHT; ++y) {
		if (jjTileGet(4, XTile, YTile + y) == 0)
			Block.left = triTRUE;
		if (jjTileGet(4, XTile + BLOCKWIDTH - 1, YTile + y) == 0)
			Block.right = triTRUE;
	}
	for (uint x = 0; x < BLOCKWIDTH; ++x)
		for (uint y = 0; y < BLOCKHEIGHT; ++y) {
			int ev = jjEventGet(XTile + x, YTile + y);
			if (isEventAStartPos(ev)) Block.start = triTRUE;
			else if (isEventAnEnd(ev)) Block.exit = triTRUE;
			else if (ev == OBJECT::STRAWBERRY) Block.store = triTRUE;
			else if (ev == OBJECT::CARROTUSPOLE) Block.pole = triTRUE;
		}
}

uint getBlockID(blockProperties@ Block, array<blockProperties>@ AvailableBlocks) {
	while(true) { //if nothing is available, you're screwed!
		uint possibleID = randRange(0, AvailableBlocks.length());
		if (Block.allows(@AvailableBlocks[possibleID])) return possibleID;
	}
	return 0; //nope
}


void transplantBlock(blockProperties@ Block,uint XDest, uint YDest, uint XSrc, uint YSrc) {
	for (uint x = 0; x < BLOCKWIDTH; ++x)
		for (uint y = 0; y < BLOCKHEIGHT; ++y) {
			jjTileSet(3, XDest + x, YDest + y, jjTileGet(3, XSrc + x, YSrc + y));
			jjTileSet(4, XDest + x, YDest + y, jjTileGet(4, XSrc + x, YSrc + y));
			jjTileSet(5, XDest + x, YDest + y, jjTileGet(5, XSrc + x, YSrc + y));
			int ev = jjEventGet(XSrc + x, YSrc + y);
			if (isEventAStartPos(ev)) {
				if (Block.start == triTRUE) {
					StartPosX = (XDest + x) * 32;
					StartPosY = (YDest + y) * 32;
				} else 
					continue;
			} else if (isEventAnEnd(ev)) {
				if (Block.exit == triTRUE) {
					jjTileSet(3, XDest + x, YDest + y, 180);
					EndPosX = XDest + x;
					EndPosY = YDest + y;
				}
				continue;
			}
			jjEventSet(XDest + x, YDest + y, ev);
			jjParameterSet(XDest + x, YDest + y, 0, 20, jjParameterGet(XSrc + x, YSrc + y, 0, 20));
			if (ev == AREA::TEXT && jjParameterGet(XSrc+x, YSrc+y, 9, 1) == 1) { //angelscript
				if (jjParameterGet(XSrc+x, YSrc+y, 0, 8) == 0) {
					uint storeNameID = randRange(0, STORENAMES.length());
					jjDebug("Spawned shop " + STORENAMES[storeNameID] + " at (" + XDest + "," + YDest +")!");
					jjParameterSet(XDest + x, YDest + y, 10, 8, storeNameID); //random param
					jjEventSet(XDest + x, YDest + y - 1, AREA::TEXT); //copy
					jjParameterSet(XDest + x, YDest + y - 1, 0, 20, jjParameterGet(XDest + x, YDest + y, 0, 20));//copy
				}
			}
			if (ev == OBJECT::GUN9POWERUP)
				jjParameterSet(XDest+x, YDest+y, 0, 8, randRange(1, STOREITEMS.length()));
		}
}

void setTopColumn (uint X, uint16 Tile) {
	if (Tile != WALLSINGLETON) Tile = 0;
	int y = BLOCKHEIGHT * AVAILABLEBLOCKROWS;
	for (int i = 0; i < DEADROWSBETWEENBLOCKSANDMAP; ++i) {
		jjTileSet(4, X, y++, Tile);
	}
}

bool isEventAStartPos(int ev) {
	return ((ev == AREA::ECHO));
}
bool isEventAnEnd(int ev) {
	return ((ev == AREA::EOL) || (ev == AREA::WARPEOL) || (ev == OBJECT::EOLPOST) || (ev == OBJECT::BONUSPOST));
}
bool isEventASceneryBlock(int ev) {
	return ((ev == OBJECT::DESTRUCTSCENERY) || (ev == OBJECT::STOMPSCENERY) || (ev == OBJECT::COLLAPSESCENERY) || (ev == OBJECT::TRIGGERSCENERY) || (ev == OBJECT::DESTRUCTSCENERYBOMB));
}

uint randRange(uint minimum = 0x0u, uint maximum = 0xFFFFFFFFu) {
	if (maximum <= minimum) return minimum;
	uint length = maximum - minimum;
	return (getRandom() % length) + minimum;
}
uint RANDSEED0 = 0;	//stored permanently and given to clients
uint RANDSEED1 = 1; //must not be zero
uint RANDSEED2 = 1; //must not be zero
uint getRandom() { //http://en.wikipedia.org/wiki/Random_number_generation
    RANDSEED1 = 18000 * (RANDSEED1 & 65535) + (RANDSEED1 >> 16);
    RANDSEED2 = 36969 * (RANDSEED2 & 65535) + (RANDSEED2 >> 16);
    return (RANDSEED2 << 16) + RANDSEED1;
}


const uint16 WALLSINGLETON = 675;
const array<uint16> WALLTILES = {
	WALLSINGLETON,
	1,2,3, 5,
	10,11,12,13,14,15,
	20,25,
	30,31,32,33,34,35,
	44,45,
	6,7,8,9,119,
	240,250,280,
	37,38,39, 46, 65,69,75,76,77,78,79,
	130,131,132,133,134,
	209, 215, 230,231,232,233,234,235,238,239,
	284,285,286,
	56, 149,
	260,606,607,
	314, 318,319, 348,349, 408,409, 495,496,
	291,296,
	40,41,42,43, 86,87,88,89,
	265,266, 248
};
const array<uint16> WALLTILESPLATFORM = {6,7,8};
const array<uint16> WALLTILESFLOOR = {1,2,3};
const array<uint16> WALLTILESLEFT = {10,20};
const array<uint16> WALLTILESRIGHT = {25,35};
const array<uint16> WALLTILESCEILING = {31,33,33,44};
const array<uint16> WALLTILESINSIDE = {11,12,13,34};
uint16 replaceWallTileAt(uint8 Layer, uint16 TileX, uint16 TileY) {
	uint16 u = jjTileGet(Layer, TileX, TileY-1);
	uint16 d = jjTileGet(Layer, TileX, TileY+1);
	uint16 l = jjTileGet(Layer, TileX-1, TileY);
	uint16 r = jjTileGet(Layer, TileX+1, TileY);
	if (isWallTile(u)) {									//wall above
		if (isWallTile(d)) {									//wall below
			if (isWallTile(l)) {									//wall to the left
				if (isWallTile(r)) {									//wall to the right
					if (isWallTile(jjTileGet(Layer, TileX-1, TileY-1))) {
						if (isWallTile(jjTileGet(Layer, TileX+1, TileY-1))) {
							return randomTileFrom(WALLTILESINSIDE);
						} else {
							return 607;
						}
					} else {
						if (isWallTile(jjTileGet(Layer, TileX+1, TileY-1))) {
							return 606;
						} else {
							return 260;
						}
					}
				} else {												//air to the right
					return randomTileFrom(WALLTILESRIGHT);
				}
			} else {												//air to the left
				if (isWallTile(r)) {									//wall to the right
					return randomTileFrom(WALLTILESLEFT);
				} else {												//air to the right
					return 250;
				}
			}
		} else {												//air below
			if (isWallTile(l)) {									//wall to the left
				if (isWallTile(r)) {									//wall to the right
					return randomTileFrom(WALLTILESCEILING);
				} else {												//air to the right
					return 45;
				}
			} else {												//air to the left
				if (isWallTile(r)) {									//wall to the right
					return 30;
				} else {												//air to the right
					return 280;
				}
			}
		}
	} else {												//air above
		if (isWallTile(d)) {									//wall below
			if (isWallTile(l)) {										//wall to the left
				if (isWallTile(r)) {										//wall to the right
					return randomTileFrom(WALLTILESFLOOR);
				} else {													//air to the right
					return 15;
				}
			} else {													//air to the left
				if (isWallTile(r)) {										//wall to the right
					return 5;
				} else {													//air to the right
					return 240;
				}
			}
		} else {									//air below
			if (isWallTile(l)) {										//wall to the left
				if (isWallTile(r)) {										//wall to the right
					return randomTileFrom(WALLTILESPLATFORM);
				} else {													//air to the right
					return 9;
				}
			} else {													//air to the left
				if (isWallTile(r)) {										//wall to the right
					return 119;
				} else {													//air to the right
					return WALLSINGLETON;
				}
			}
		}
	}
}
bool isWallTile(uint16 Tile) {
	return (WALLTILES.find(Tile) >= 0);
}
uint16 randomTileFrom(array<uint16> Tiles) {
	return Tiles[randRange(0, Tiles.length())];
}

float myAbs(float f) {
	if (f < 0) return -f;
	return f;
}
int min(int a, int b) {
	if (a < b) return a;
	return b;
}
const float OBJECTROTATIONSPEED = 4;
void monitorActiveObjects() {
	for(int i=1; i<jjObjectCount; ++i) {
		jjOBJ@ obj = jjObjects[i];
		if(obj.isActive) {
			uint8 eventID = obj.eventID;
			if (eventID==OBJECT::TNT) {
				if (obj.state==STATE::EXPLODE && obj.counter==20) { //ta, SE
					destroyTileAt(obj.xPos, obj.yPos, 256);
					for (int angle = 0; angle < 1024; angle += 32) {
						destroyTileAt(obj.xPos + 10 + jjSin(angle) * 32, obj.yPos + 10 + jjCos(angle) * 32, angle);
						destroyTileAt(obj.xPos + 10 + jjSin(angle) * 64, obj.yPos + 10 + jjCos(angle) * 64, angle);
					}
				} else obj.state = STATE::EXPLODE;
			} else if (eventID==OBJECT::GUN9POWERUP) { //store item
				if (obj.state != STATE::KILL) {
					if (obj.var[10] == 0) { //unassigned
						obj.var[10] = jjParameterGet(obj.xOrg/32, obj.yOrg/32, 0, 8);
						obj.curAnim = jjObjectPresets[STOREITEMS[obj.var[10]][0]].curAnim;
						//obj.yPos = obj.yPos - 32; //room to fall
					}
					int dollarID = obj.var[11];
					jjOBJ@ obj2 = jjObjects[dollarID];
					if (dollarID == 0 || !obj2.isActive || obj2.eventID != OBJECT::THING) {
						obj.var[11] = dollarID = jjAddObject(OBJECT::THING, 0, 0);
						@obj2 = jjObjects[dollarID];
					}
					obj2.xPos = obj.xPos - 8;
					obj2.yPos = obj.yPos - 32 - 8;
					obj2.frameID = 4;
					obj2.determineCurFrame();
					obj2.frameID = 3; //in case the native code tries to advance it one frame while I'm not looking
					if (jjGameConnection != GAME::LOCAL) if (obj.state == STATE::FALL) obj.state = STATE::SLEEP;
					for (int j = 0; j < jjLocalPlayerCount; ++j) {
						jjPLAYER@ play = jjLocalPlayers[j];
						if (play.xPos >= obj.xPos - 5 && play.xPos <= obj.xPos + 5 && play.yPos >= obj.yPos - 96 && play.yPos <= obj.yPos + 16)
							play.testForCoins(STOREITEMS[obj.var[10]][1] * COINMULTIPLIER + play.coins); //will always be false, but helpfully displays the price
					}
					//STOREITEMS[Var][1] * COINMULTIPLIER
				} else { //being destroyed
					int dollarID = obj.var[11];
					jjOBJ@ obj2 = jjObjects[dollarID];
					if (obj2.isActive && obj2.eventID == OBJECT::THING)
						jjDeleteObject(dollarID);
					jjDeleteObject(i);
					for (int i = 0; i < jjLocalPlayerCount; ++i) {
						jjPLAYER@ play = jjLocalPlayers[i];
						if (play.powerup[WEAPON::GUN9]) { //the culprit!
							play.powerup[WEAPON::GUN9] = false;
							play.ammo[WEAPON::GUN9] = 0;
							givePlayerBenefitOfPurchasedStoreItem(@play, obj.var[10]);
						}
					}
				}
			} else if (eventID==OBJECT::STRAWBERRY) { //aka Bubba
				if (obj.energy < 100) {
					if (obj.energy > 50) { //convert Angel to Devil
						BubbaEnraged = true;
						jjMusicLoad("dang.j2b");
						//for (int i = 0; i < jjLocalPlayerCount; ++i)
						//	jjLocalPlayers[i].showText("@@@@You'll pay for that!");
						obj.determineCurAnim(ANIM::BUBBA, 6);
						obj.noHit = 16;//unhurtable, not affected by TNT
						obj.var[9] = obj.xPos; obj.var[10] = obj.yPos; //retarget right away
					}
					obj.energy = 50;
					if (myAbs(obj.var[9] - obj.xPos) < 12 && myAbs(obj.var[10] - obj.yPos) < 12) { //retarget
						float tX = jjLocalPlayers[0].xPos; obj.var[9] = tX;
						float tY = jjLocalPlayers[0].yPos; obj.var[10] = tY;
						float mX = obj.xPos; float mY = obj.yPos;
						float dX = tX - mX; float dY = tY - mY;
						if (myAbs(dX) > myAbs(dY)) {
							if (dX > 0) obj.xAcc = BUBBASPEED;
							else obj.xAcc = -BUBBASPEED;
							obj.yAcc = (myAbs(dY) / myAbs(dX)) * BUBBASPEED;
							if (dY < 0) obj.yAcc = -obj.yAcc;
						} else {
							if (dY > 0) obj.yAcc = BUBBASPEED;
							else obj.yAcc = -BUBBASPEED;
							obj.xAcc = (myAbs(dX) / myAbs(dY)) * BUBBASPEED;
							if (dX < 0) obj.xAcc = -obj.xAcc;
						}
						obj.direction = obj.xAcc;
					} else {
						obj.state = STATE::FLOAT;
						obj.xPos = obj.xPos + obj.xAcc;
						obj.yPos = obj.yPos + obj.yAcc;
					}
				}
			} else if (eventID == OBJECT::BOMB && obj.creatorType == CREATOR::LEVEL) { //Purple Gem, Damsel
				obj.counterEnd = 100; //arbitrarily large number
				obj.objType = 8 | 64;
				obj.noHit = 17;
				obj.eventID = OBJECT::FIVEHUNDREDBUMP; //let's see what happens
				obj.lightType = LIGHT::LASER;
				obj.light = 15;
				obj.killAnim = jjObjectPresets[OBJECT::PURPLEGEM].killAnim;
				if (jjParameterGet(obj.xOrg / 32, obj.yOrg / 32, 0, 1) == 0) {
					obj.doesHurt = OBJECT::PURPLEGEM;
					obj.curAnim = jjObjectPresets[OBJECT::SUPERGEM].curAnim;
					obj.var[0] = 4;
					obj.xPos = obj.xPos + 18;
				} else {
					obj.doesHurt = OBJECT::EVA;
					obj.determineCurAnim(ANIM::EVA, 1);
					obj.yPos = obj.yPos - 9;
				}
			} else if (obj.doesHurt > 0) {
				obj.counter = 0; //prevent it from ever exploding
				if (obj.creatorType == CREATOR::PLAYER) {
					jjPLAYER@ play = jjPlayers[obj.creator];
					if (play.health <= 0) { jjDeleteObject(i); continue; } //dead
					float objectRotationSpeed = -1.5 * play.xSpeed;
					if (myAbs(objectRotationSpeed) < OBJECTROTATIONSPEED) objectRotationSpeed =
						(play.direction >= 0) ? -OBJECTROTATIONSPEED : OBJECTROTATIONSPEED;
					obj.age += objectRotationSpeed;
					int range = obj.var[1];
					obj.xPos = play.xPos + jjSin(obj.age) * range;
					if (obj.xPos < play.xPos) obj.direction = -1; else obj.direction = 1;
					obj.yPos = play.yPos + jjCos(obj.age) * range;
					if (obj.state == STATE::ACTION) {
						if (obj.doesHurt == OBJECT::EVA)
							play.showText("@@@@@@#I trusted you!");
						else
							play.showText("@@@@@@#Fool!");
						jjDeleteObject(i);
						assignAnglesToAllObjectsSurroundingPlayer(obj.creator - 0x8000);
					}
					if (range < 64 && range > 0) obj.var[1] = (range - 2);
				} else if (obj.state == STATE::ACTION) { //touched by a player
					obj.state = STATE::SLEEP;
					obj.eventID = OBJECT::ICEAMMO15; //shootable but not touchable
					obj.creator = obj.var[0] + 0x8000; //player
					assignAnglesToAllObjectsSurroundingPlayer(obj.var[0]);
					obj.var[0] = 4; //purple gem
					obj.var[1] = 64; //range
					obj.noHit = 16;
					obj.energy = 1;
					jjPlayers[obj.creator].showText("@@@@@@#Deliver me to the exit!");
				} else if (obj.doesHurt == OBJECT::EVA) {
					if (obj.xPos < jjLocalPlayers[0].xPos) obj.direction = -1;
					else obj.direction = 1;
				}
			}
		}
	}
}
void destroyTileAt(float X, float Y, int angle) {
	int16 xTile = X / 32;
	int16 yTile = Y / 32;
	if (yTile < jjLayerHeight[4] - (BLOCKHEIGHT * MAPHEIGHT)) return;
	if (yTile >= jjLayerHeight[4] - 1) return;
	bool alreadyEnraged = false;
	if ((jjTileGet(4,xTile,yTile) != 0)) {
		if ((jjRandom() & 7) == 0) {
			int o = jjAddObject(OBJECT::FLICKERGEM, X, Y);
			if (o > 0) { //for what it's worth
				jjOBJ@ coin = jjObjects[o];
				coin.state = STATE::FLOATFALL;
				coin.xSpeed = jjSin(angle) * 4;
				coin.ySpeed = jjCos(angle) * 4;
				if ((jjRandom() & 3) == 0) {
					coin.curAnim = jjObjectPresets[OBJECT::GOLDCOIN].curAnim;
					coin.var[0] = jjObjectPresets[OBJECT::GOLDCOIN].var[0];
					coin.points *= 5;
				}
			}
		}
		int16 xBlock = xTile / BLOCKWIDTH;
		int16 yBlock = (yTile - DEADROWSBETWEENBLOCKSANDMAP - BLOCKHEIGHT*AVAILABLEBLOCKROWS) / BLOCKHEIGHT;
		if (xBlock == StoreX && yBlock == StoreY)
			enrageBubba("Die, you vandal!");
	}
	jjTileSet(3, xTile, yTile, 0);
	jjTileSet(4, xTile, yTile, 0);
	jjTileSet(5, xTile, yTile, 0);

}

void enrageBubba(string shout) {
	for(int i=1; i<jjObjectCount; ++i) {
		jjOBJ@ obj = jjObjects[i];
		if(obj.isActive && obj.eventID==OBJECT::STRAWBERRY && obj.energy > 50)
			obj.energy = 75;
	}
	for (int i = 0; i < jjLocalPlayerCount; ++i)
		jjLocalPlayers[i].showText("@@@@" + shout);
}

void shouldTheLevelBeCyclingRightAboutNow() {
	if (jjGameTicks >= TimeToCycleForReal) { //can't rely on jjNxt online, so use this instead...
		TimeToCycleForReal = 0x7FFFFFFF;
		jjChat("/r");
		return;
	}
	if (BegunCyclingAlready) return;
	for (int i = 0; i < 32; ++i) {
		jjPLAYER@ play = jjPlayers[i];
		if (play.isActive) {
			if ((uint16(play.xPos / 32) == EndPosX) && (uint16(play.yPos / 32) == EndPosY)) {
				for (int j = 0; j < jjLocalPlayerCount; ++j) {
					@play = jjLocalPlayers[j];
					play.timerStop(); //JJ2 should do this on its own, but doesn't atm :(
					play.ammo[WEAPON::ICE] = play.health;
				}
				BegunCyclingAlready = true;
				if (jjGameConnection != GAME::LOCAL && jjIsServer) {
					TimeToCycleForReal = jjGameTicks + 70; //give some time for _clients_ to notice the level is ending
					jjChat("|Player " + (i+1) + " found the exit!");
				}
				jjNxt();
				for(int j=1; j<jjObjectCount; ++j) {
					jjOBJ@ obj = jjObjects[j];
					if (obj.isActive && obj.eventID == OBJECT::ICEAMMO15 && (obj.creator - 0x8000) == i) {
						if (obj.doesHurt == OBJECT::PURPLEGEM) {
							obj.points = 50*COINMULTIPLIER;
							obj.eventID = OBJECT::GOLDCOIN;
						} else if (obj.doesHurt == OBJECT::EVA) {
							obj.eventID = OBJECT::CARROT;
							++obj.curAnim;
							jjPlayers[i].ammo[WEAPON::ICE] = jjPlayers[i].ammo[WEAPON::ICE] + 1; //add one health
						}
						obj.objType = 5;
						obj.var[1] = obj.var[1] - 2;
					}
				}
			}
		}
	}
}

void assignAnglesToAllObjectsSurroundingPlayer(int PlayerID) {
	int numberOfSuchObjects = 0;
	for(int i=1; i<jjObjectCount; ++i) {
		jjOBJ@ obj = jjObjects[i];
		if (obj.isActive && obj.eventID == OBJECT::ICEAMMO15 && (obj.creator - 0x8000) == PlayerID)
			++numberOfSuchObjects;
	}
	if (numberOfSuchObjects > 0) {
		int angleInterval = 1024 / numberOfSuchObjects;
		numberOfSuchObjects = 0;
		for(int i=1; i<jjObjectCount; ++i) {
			jjOBJ@ obj = jjObjects[i];
			if (obj.isActive && obj.eventID == OBJECT::ICEAMMO15 && (obj.creator - 0x8000) == PlayerID)
				obj.age = numberOfSuchObjects++ * angleInterval;
		}
	}
}

const uint16 COINMULTIPLIER = 1; //just for show
void onLevelLoad() {

	for (int i = 0; i < 256; ++i)
		jjObjectPresets[i].points = 0;

	jjOBJ@ obj;
	jjOBJ@ model;
	@model = jjObjectPresets[OBJECT::SILVERCOIN];
	model.points = 1*COINMULTIPLIER;
	@obj = jjObjectPresets[OBJECT::REDGEM];
	obj.curAnim = model.curAnim;
	obj.var[0] = model.var[0]; //sprite displayed
	obj.eventID = model.eventID;
	obj.points = model.points;
	@obj = jjObjectPresets[OBJECT::FLICKERGEM];
	obj.curAnim = model.curAnim;
	obj.var[0] = model.var[0];
	obj.eventID = model.eventID;
	obj.points = model.points;
	@obj = jjObjectPresets[OBJECT::GREENGEM];
	obj.curAnim = model.curAnim;
	obj.var[0] = model.var[0];
	obj.eventID = model.eventID;
	obj.points = model.points;
	@obj = jjObjectPresets[OBJECT::CARROT];
	obj.curAnim = model.curAnim;
	obj.var[0] = model.var[0];
	obj.eventID = model.eventID;
	obj.points = model.points;
	@model = jjObjectPresets[OBJECT::GOLDCOIN];
	model.points = 5*COINMULTIPLIER;
	@obj = jjObjectPresets[OBJECT::BLUEGEM];
	obj.curAnim = model.curAnim;
	obj.var[0] = model.var[0];
	obj.eventID = model.eventID;
	obj.points = model.points;
	
	@obj = jjObjectPresets[OBJECT::STRAWBERRY];
	obj.determineCurAnim(ANIM::BUBBA, 1);
	obj.objType = 16 | 64; //enemy who never leaves
	obj.energy = 100; //or whatever
	obj.lightType = LIGHT::POINT2;
	obj.light = 1;
	
	@obj = jjObjectPresets[OBJECT::GUN9POWERUP]; //store item
	obj.noHit = 1 | 4 | 8 | 16; //generally immune to bullets
	obj.lightType = LIGHT::BRIGHT;
	obj.light = 10;
	@obj = jjObjectPresets[OBJECT::THING]; //floating $ icon
	obj.objType = 9; //not interact-with-able
	obj.determineCurAnim(ANIM::FONT, 1);
	
	jjObjectPresets[OBJECT::BOMB].objType = 32; //enemy
	
	jjPalette = jjBackupPalette;
	jjPalette.gradient(165,243,194, 19,57,32, 96, 8); //grass
	jjPalette.gradient(230,213,198, 21,15,10, 104, 8); //soil
	jjPalette.gradient(186,238,201, 20,62,29, 112, 16); //carrots
	jjPalette.gradient(185,143,233, 59,37,83); //sky
	jjPalette.fill(0,0,255, 160,16, .25); //BG trees
	jjPalette.fill(255,255,255, 16,16*6, .16); //sprites
	jjSetFadeColors();
	jjPalette.apply();
}

const array<GEM::Color> GEMCOLORS = {GEM::RED, GEM::GREEN, GEM::BLUE};
const array<int> GEMCOINNUMBERS = {10, 15, 25};
array<bool> recentDeath(4, false);
void onPlayer() {
	if (jjGameConnection != GAME::LOCAL) {
		if (p.health <= 0) //ded
			recentDeath[p.localPlayerID] = true;
	}
	if (recentDeath[p.localPlayerID] && p.health > 0) {
		recentDeath[p.localPlayerID] = false;
		p.timerStart(70 * (60+60+30));
		if (p.ammo[WEAPON::ICE] > 0 && jjGameTicks < 70*6) { //continue from previous level
			if (p.powerup[WEAPON::ICE]) p.fly = FLIGHT::AIRBOARD;
			p.health = p.ammo[WEAPON::ICE];
			p.ammo[WEAPON::ICE] = 0;
		} else { //init, or reinit after a multiplayer death
			for (int i = 0; i < 9; ++i) {
				p.ammo[i] = 0;
				p.powerup[i] = false;
			}
			p.ammo[WEAPON::TNT] = 4;
			p.ammo[WEAPON::BLASTER] = 50; //arbitrary non-zero number. otherwise it can't be switched to (except by using the 1 key) and the first time you shoot doesn't work.
			p.currWeapon = WEAPON::BLASTER;
			p.food = p.lives = p.score = p.coins = 0;
		}
	}
	if (p.coins != p.score) p.coins = p.score;	//allows for the use of arbitrary increase values, and also
												//allows coin count to persist between levels
	for (int i = 0; i < 3; ++i) 		//rectgems will display in-level as gems, but show
		if (p.gems[GEMCOLORS[i]] > 0) {	//the coin icon at the bottom of the screen
			p.gems[GEMCOLORS[i]] = 0;
			jjObjects[jjAddObject(OBJECT::GOLDCOIN, p.xPos, p.yPos)].points = COINMULTIPLIER*GEMCOINNUMBERS[i];
		}
}

const array<array<uint8>> STOREITEMS = { {0,0},
	{OBJECT::FASTFIRE, 5}, {OBJECT::MORPH, 3}, {OBJECT::FULLENERGY, 40}, {OBJECT::TNTAMMO3, 7},
	{OBJECT::BLASTERPOWERUP, 100}, {OBJECT::BOUNCERPOWERUP, 75}, {OBJECT::SEEKERPOWERUP, 100}, {OBJECT::RFPOWERUP, 50}, {OBJECT::TOASTERPOWERUP, 75},
	{OBJECT::BOUNCERAMMO15, 5}, {OBJECT::SEEKERAMMO15, 7}, {OBJECT::RFAMMO15, 5}, {OBJECT::TOASTERAMMO15, 5},
	{OBJECT::FIRESHIELD, 25}, {OBJECT::BUBBLESHIELD, 30}, {OBJECT::PLASMASHIELD, 35},
	{OBJECT::AIRBOARD, 30}, {OBJECT::FLYCARROT, 10}
};
const array<string> STOREITEMNAMES = {"",
	"Some fastfire", "A morph", "Full health", "Some TNT",
	"A blaster powerup", "A bouncer powerup", "A seeker powerup", "An RF powerup", "A toaster powerup",
	"Some bouncer ammo", "Some seeker ammo", "Some RF ammo", "Some toaster ammo",
	"A fire shield", "A bubble shield", "A lightning shield",
	"An airboard", "A fly carrot"
};
void givePlayerBenefitOfPurchasedStoreItem(jjPLAYER@ play, int Var) {
	if (!BubbaEnraged && play.testForCoins(STOREITEMS[Var][1] * COINMULTIPLIER)) {
		play.showText("@@@" + STOREITEMNAMES[Var] + " for " + STOREITEMS[Var][1] * COINMULTIPLIER + " coins!@Thank you, come again!");
		play.score = play.coins;
	} else
		enrageBubba("Get back here, thief!");
	switch(STOREITEMS[Var][0]) {
		case OBJECT::FASTFIRE:
			play.fastfire -= 3;
			if (play.fastfire < 6) play.fastfire = 6;
			break;
		case OBJECT::MORPH:
			play.morph(true, true);
			break;
		case OBJECT::FULLENERGY:
			play.health = jjMaxHealth;
			play.invincibility = 70 * 10; //positive number = visual effect, and it even works in offline play
			break;
		case OBJECT::TNTAMMO3:
			if (play.ammo[WEAPON::TNT] == 0) play.currWeapon = WEAPON::TNT;
			play.ammo[WEAPON::TNT] = play.ammo[WEAPON::TNT] + 3; //who cares about limits
			break;
		case OBJECT::BLASTERPOWERUP:
			if (!play.powerup[WEAPON::BLASTER]) {
				play.currWeapon = WEAPON::BLASTER;
				play.powerup[WEAPON::BLASTER] = true;
			}
			break;
		case OBJECT::BOUNCERPOWERUP:
			if (!play.powerup[WEAPON::BOUNCER]) {
				play.currWeapon = WEAPON::BOUNCER;
				play.powerup[WEAPON::BOUNCER] = true;
			}
			play.ammo[WEAPON::BOUNCER] = min(play.ammo[WEAPON::BOUNCER] + 20, 99);
			break;
		case OBJECT::SEEKERPOWERUP:
			if (!play.powerup[WEAPON::SEEKER]) {
				play.currWeapon = WEAPON::SEEKER;
				play.powerup[WEAPON::SEEKER] = true;
			}
			play.ammo[WEAPON::SEEKER] = min(play.ammo[WEAPON::SEEKER] + 20, 99);
			break;
		case OBJECT::RFPOWERUP:
			if (!play.powerup[WEAPON::RF]) {
				play.currWeapon = WEAPON::RF;
				play.powerup[WEAPON::RF] = true;
			}
			play.ammo[WEAPON::RF] = min(play.ammo[WEAPON::RF] + 20, 99);
			break;
		case OBJECT::TOASTERPOWERUP:
			if (!play.powerup[WEAPON::TOASTER]) {
				play.currWeapon = WEAPON::TOASTER;
				play.powerup[WEAPON::TOASTER] = true;
			}
			play.ammo[WEAPON::TOASTER] = min(play.ammo[WEAPON::TOASTER] + 20, 99);
			break;
		case OBJECT::BOUNCERAMMO15:
			if (play.ammo[WEAPON::BOUNCER] == 0)
				play.currWeapon = WEAPON::BOUNCER;
			play.ammo[WEAPON::BOUNCER] = min(play.ammo[WEAPON::BOUNCER] + 15, 99);
			break;
		case OBJECT::SEEKERAMMO15:
			if (play.ammo[WEAPON::SEEKER] == 0)
				play.currWeapon = WEAPON::SEEKER;
			play.ammo[WEAPON::SEEKER] = min(play.ammo[WEAPON::SEEKER] + 15, 99);
			break;
		case OBJECT::RFAMMO15:
			if (play.ammo[WEAPON::RF] == 0)
				play.currWeapon = WEAPON::RF;
			play.ammo[WEAPON::RF] = min(play.ammo[WEAPON::RF] + 15, 99);
			break;
		case OBJECT::TOASTERAMMO15:
			if (play.ammo[WEAPON::TOASTER] == 0)
				play.currWeapon = WEAPON::TOASTER;
			play.ammo[WEAPON::TOASTER] = min(play.ammo[WEAPON::TOASTER] + 15, 99);
			break;
		case OBJECT::FIRESHIELD:
			play.currWeapon = 0;
			play.shieldTime = 30 * 70;
			play.shieldType = 1;
			break;
		case OBJECT::WATERSHIELD:
			play.currWeapon = 0;
			play.shieldTime = 40 * 70;
			play.shieldType = 2;
			break;
		case OBJECT::PLASMASHIELD:
			play.currWeapon = 0;
			play.shieldTime = 45 * 70;
			play.shieldType = 3;
			break;
		case OBJECT::FLYCARROT:
			play.fly = FLIGHT::FLYCARROT;
			break;
		case OBJECT::AIRBOARD:
			play.fly = FLIGHT::AIRBOARD;
			play.powerup[WEAPON::ICE] = true; //permanent
			break;
	}
	if (play.currWeapon == WEAPON::GUN9) play.currWeapon = WEAPON::BLASTER;
}

const array<string> STORENAMES = {
	"MacLellan's Pub and Grill", "Nick's Cafe", "Clifford Terrace", "Allen's Groceries",
	"Brussee's Bistro", "Dodrill Optical", "Michiel's Arcade", "Epic Pinball Arcade", "One Must Fall Pay Per View Arena"
};
void onFunction0(uint8 name) {
	if (!BubbaEnraged) p.showText("@@@@Hello, welcome to@#" + STORENAMES[name] + "!");
}


void onPlayerTimerEnd() {
	if (jjGameConnection != GAME::LOCAL) ++p.lives;
	p.kill();
}