Name | Author | Game Mode | Rating | |||||
---|---|---|---|---|---|---|---|---|
EmeraldusV | Violet CLM | Multiple | 8.9 |
#include "MLLE-Include-1.4.asc"
const bool MLLESetupSuccessful = MLLE::Setup();
#pragma require "EmeraldusV-MLLE-Data-1.j2l"
#pragma require "EmeraldusV.j2l"
#pragma require "EmeraldusV.j2a"
enum AnimSets { BALL, BOSS, BRIDGE, BUZZBOMBER, CHOPPER, CRABMEAT, MOTOBUG, NEWTRON, PLATFORM, TREES, TURTLETTE, _LAST };
array<jjLAYER@> Ripples;
jjLAYER@ Clouds;
const uint8 CornerEventID = OBJECT::APPLE; //whatever
void onLevelLoad() {
jjLAYER@ ripples = MLLE::GetLayer("Ripples");
auto layerOrder = jjLayerOrderGet();
const auto index = layerOrder.findByRef(ripples);
int numberOfLayersToAdd = 24; //adjust as needed based on layer speed/level height
while (true) {
Ripples.insertLast(ripples);
layerOrder.insertAt(index, ripples);
if (--numberOfLayersToAdd > 0) {
@ripples = jjLAYER(ripples);
ripples.xOffset = jjRandom() & ((16*32) - 1);
ripples.yOffset -= 16;
} else
break;
}
jjLayerOrderSet(layerOrder);
jjUseLayer8Speeds = true;
@Clouds = MLLE::GetLayer("Clouds");
jjObjectPresets[CornerEventID].behavior = BEHAVIOR::INACTIVE; //zonify
jjObjectPresets[OBJECT::ORANGE].behavior = LoopObject;
jjObjectPresets[OBJECT::ORANGE].playerHandling = HANDLING::PARTICLE;
AddCornerEvents();
for (uint i = 0; i < AnimSets::_LAST; ++i)
jjAnimSets[ANIM::CUSTOM[i]].load(i, "EmeraldusV.j2a");
if (jjAnimSets[ANIM::BRIDGE] == 0)
jjAnimSets[ANIM::BRIDGE].load();
jjAnimations[jjAnimSets[ANIM::BRIDGE] + 4] = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::BRIDGE]]];
jjObjectPresets[OBJECT::BRIDGE].behavior = MyBridge;
Crabmeat(jjObjectPresets[OBJECT::BANANA]);
Motobug(jjObjectPresets[OBJECT::CHEESE]);
Ball(jjObjectPresets[OBJECT::BURGER]);
GreenNewtron(jjObjectPresets[OBJECT::CHICKENLEG]);
BlueNewtron(jjObjectPresets[OBJECT::CHIPS]);
Chopper(jjObjectPresets[OBJECT::EGGPLANT]);
BuzzBomber(jjObjectPresets[OBJECT::FRIES]);
Turtlette(jjObjectPresets[OBJECT::COKE]);
jjOBJ@ tree = jjObjectPresets[OBJECT::CHERRY];
tree.behavior = Tree;
tree.playerHandling = HANDLING::PARTICLE;
tree.determineCurAnim(ANIM::CUSTOM[AnimSets::TREES], 0);
MakeTreeObjects();
if (jjAnimSets[ANIM::PINKPLAT] == 0)
jjAnimSets[ANIM::PINKPLAT].load();
for (int i = 0; i < 2; ++i)
jjAnimations[jjAnimSets[ANIM::PINKPLAT] + i] = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::PLATFORM]] + i];
jjObjectPresets[OBJECT::PINKPLATFORM].behavior = PlatformWrapper;
jjObjectPresets[OBJECT::GRASSPLATFORM].curFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::PLATFORM]] + 3];
jjObjectPresets[OBJECT::GRASSPLATFORM].behavior = GrassPlatform;
jjObjectPresets[OBJECT::GRASSPLATFORM].playerHandling = HANDLING::PARTICLE;
jjObjectPresets[OBJECT::WEENIE].behavior = Ropeway();
jjObjectPresets[OBJECT::WEENIE].scriptedCollisions = true;
jjObjectPresets[OBJECT::WEENIE].curFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::PLATFORM]] + 4];
jjObjectPresets[OBJECT::GREENGEM].behavior = BEHAVIOR::INACTIVE;
jjObjectPresets[OBJECT::BLUEGEM].behavior = BEHAVIOR::INACTIVE;
jjObjectPresets[OBJECT::FROZENSPRING].freeze = 0;
jjObjectPresets[OBJECT::FROZENSPRING].ySpeed /= 3;
jjObjectPresets[OBJECT::FROZENSPRING].behavior = InvisibleSpring;
Boss(jjObjectPresets[OBJECT::CAKE]);
}
void onDrawLayer8(jjPLAYER@ player, jjCANVAS@) {
if (player.localPlayerID != 0 && player.isLocal)
return;
for (uint i = 0; i < Ripples.length; ++i)
Ripples[i].xOffset += 0.03 * (i + 1);
Clouds.xOffset += 0.25;
}
const float RightEdgeOfLevel = jjLayerWidth[4] * 32;
class Enemy : jjBEHAVIORINTERFACE {
Enemy(jjOBJ@ preset, int animSpeed = 1) {
preset.behavior = this;
preset.playerHandling = HANDLING::ENEMY;
preset.bulletHandling = HANDLING::HURTBYBULLET;
preset.direction = 1;
preset.animSpeed = animSpeed;
preset.curFrame = jjAnimations[preset.curAnim] + preset.frameID;
preset.isTarget = true;
if (jjDifficulty >= 3)
preset.energy += 1;
}
void onBehave(jjOBJ@ obj) {}
void onDraw(jjOBJ@ obj) {}
bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) { return true; }
void spawnTurtlette(jjOBJ@ obj) const {
jjSample(obj.xPos, obj.yPos, SOUND::COMMON_GEMSMSH1, 63, 5000);
jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos)].determineCurAnim(ANIM::AMMO, 2);
jjAddObject(OBJECT::COKE, obj.xPos, obj.yPos, obj.creatorID, CREATOR::OBJECT);
}
}
class WalkingEnemy : Enemy {
int yDist;
int halfFrameWidth;
int walkingAnimationSpeed;
WalkingEnemy(jjOBJ@ preset, int animSpeed) {
super(preset, animSpeed);
walkingAnimationSpeed = animSpeed;
const jjANIMFRAME@ frame = jjAnimFrames[preset.curFrame];
halfFrameWidth = frame.width / 2;
yDist = int(abs(frame.coldSpotY - frame.hotSpotY)) + 2;
}
void onBehave(jjOBJ@ obj) {
switch (obj.state) {
case STATE::START:
obj.putOnGround(true);
obj.state = STATE::WALK;
break;
case STATE::KILL:
spawnTurtlette(obj);
obj.delete();
break;
case STATE::DEACTIVATE:
obj.deactivate();
break;
case STATE::FREEZE:
if (--obj.freeze == 0)
obj.state = obj.oldState;
break;
default: {
obj.xAcc += obj.xSpeed;
while (obj.xAcc >= 1.f) {
const float testHPos = obj.xPos + obj.direction;
if (testHPos <= 0 || testHPos >= RightEdgeOfLevel)
obj.direction = -obj.direction;
else {
const int topYPos = jjMaskedTopVLine(int(testHPos), int(obj.yPos), yDist);
//jjDrawRectangle(testHPos, obj.yPos, 3,3, 16, SPRITE::NORMAL,0,2);
bool falling = false;
if (topYPos <= 1 || ((falling = (topYPos >= yDist)) && obj.yAcc <= 0) || jjEventAtLastMaskedPixel == AREA::STOPENEMY) {
obj.direction = -obj.direction;
} else { //walk forwards
obj.xPos = testHPos;
if (falling)
obj.state = STATE::FALL;
else
obj.yPos += topYPos - (yDist - 2);
}
}
obj.xAcc -= 1;
}
if (obj.state == STATE::FALL) {
obj.yPos += obj.ySpeed += obj.yAcc;
while (jjMaskedTopVLine(int(obj.xPos), int(obj.yPos), yDist) < yDist) {
obj.state = STATE::WALK;
obj.yPos -= 1;
}
if (obj.state == STATE::WALK) {
obj.yPos += 1;
obj.ySpeed = 0;
}
}
obj.special = 0; //drawing angle
if (obj.state == STATE::WALK) {
const int yDist1 = jjMaskedTopVLine(int(obj.xPos - 9), int(obj.yPos), 50);
if (yDist1 != 1 && yDist1 != 51) {
const int yDist2 = jjMaskedTopVLine(int(obj.xPos + 9), int(obj.yPos), 50);
if (yDist2 != 1 && yDist2 != 51) {
obj.special = int(atan2(yDist1 - yDist2, 19) / 6.28318531 * 1024);
}
}
}
if (--obj.animSpeed < 0) {
obj.animSpeed = walkingAnimationSpeed;
obj.frameID += 1;
obj.determineCurFrame();
}
break; }
}
}
void onDraw(jjOBJ@ obj) {
jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.special, obj.direction,1, (obj.justHit == 0) ? (obj.state != STATE::FREEZE) ? SPRITE::NORMAL : SPRITE::FROZEN : SPRITE::SINGLECOLOR, 15);
}
}
void AddEnemyBullet(const jjOBJ@ obj, float xSpeed, float ySpeed, float yAcc, bool sample = true) {
const jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
jjOBJ@ bullet = jjObjects[jjAddObject(OBJECT::BULLET, obj.xPos + obj.direction * (frame.hotSpotX - frame.gunSpotX), obj.yPos + frame.hotSpotY - frame.gunSpotY, obj.objectID, CREATOR::OBJECT, EnemyBullet)];
bullet.xSpeed = xSpeed;
bullet.ySpeed = ySpeed;
bullet.yAcc = yAcc;
bullet.curFrame = jjAnimations[jjAnimSets[ANIM::CUSTOM[AnimSets::NEWTRON]] + 1];
bullet.animSpeed = 1;
bullet.playerHandling = HANDLING::ENEMYBULLET;
bullet.light = 1;
bullet.lightType = LIGHT::POINT2;
if (sample)
jjSample(bullet.xPos, bullet.yPos, SOUND::COMMON_GLASS2, 63, 30000);
else
bullet.counterEnd = 172;
}
void EnemyBullet(jjOBJ@ obj) {
if (obj.state == STATE::START)
obj.state = STATE::FLY;
else if (obj.state == STATE::DEACTIVATE)
obj.delete();
else if (obj.state == STATE::EXPLODE || ++obj.counterEnd >= 210) {
obj.curAnim = jjAnimSets[ANIM::AMMO] + 72;
obj.behavior = BEHAVIOR::EXPLOSION;
obj.playerHandling = HANDLING::EXPLOSION;
jjSample(obj.xPos, obj.yPos, SOUND::COMMON_EXPSM1);
} else {
obj.xPos += obj.xSpeed;
obj.yPos += obj.ySpeed += obj.yAcc;
jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos + jjSin(obj.counterEnd << 4) * 2, obj.curFrame + ((jjGameTicks >> 1) & 3), int(obj.xSpeed), SPRITE::NORMAL,0, 3);
}
}
class Motobug : WalkingEnemy {
Motobug(jjOBJ@ preset) {
preset.determineCurAnim(ANIM::CUSTOM[AnimSets::MOTOBUG], 0);
preset.energy = 3;
preset.points = 200;
preset.xSpeed = 1.25;
super(preset, 9);
}
void onBehave(jjOBJ@ obj) override {
if (obj.state == STATE::WALK) {
if (jjGameTicks & 31 == 0)
jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::COMMON_LANDCAN1 + ((jjGameTicks >> 5) & 1)));
if (obj.counter == 0) {
const auto nearestPlayerID = obj.findNearestPlayer(160*160);
if (nearestPlayerID >= 0) {
obj.counter = 1;
obj.direction = (jjPlayers[nearestPlayerID].xPos > obj.xPos) ? 1 : -1;
jjSample(obj.xPos, obj.yPos + yDist, SOUND::COMMON_REVUP);
}
} else if (obj.counter < 70) {
obj.curFrame = jjAnimations[obj.curAnim] + (++obj.counter & 1);
jjPARTICLE@ part = jjAddParticle(PARTICLE::SPARK);
if (part !is null) {
part.xPos = obj.xPos;
part.yPos = obj.yPos + yDist;
part.xSpeed = obj.direction * (-1 - (jjRandom() & 3) / 2.f);
part.ySpeed = (jjRandom() & 31) / 32.f;
}
return;
} else if (obj.counter == 70) {
obj.counter = 71;
obj.xSpeed = 10;
obj.yAcc = 0.2;
} else {
if (obj.xSpeed > jjObjectPresets[obj.eventID].xSpeed)
obj.xSpeed -= 0.1;
else {
obj.counter = 0;
obj.yAcc = 0;
}
}
}
WalkingEnemy::onBehave(obj);
}
}
class Crabmeat : WalkingEnemy {
Crabmeat(jjOBJ@ preset) {
preset.determineCurAnim(ANIM::CUSTOM[AnimSets::CRABMEAT], 0);
preset.energy = 1;
preset.points = 500;
preset.xSpeed = 1;
super(preset, 9);
}
void onBehave(jjOBJ@ obj) override {
if (obj.state != STATE::WALK || ++obj.counter < 140) {
WalkingEnemy::onBehave(obj);
if (obj.state == STATE::WALK && jjGameTicks & 31 == 0)
jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::UTERUS_SCISSORS1 + (jjRandom() & 3)));
} else if (obj.counter == 140)
obj.curFrame = jjAnimations[obj.curAnim + 1];
else if (obj.counter == 155)
for (int i = -1; i < 2; i += 2)
AddEnemyBullet(obj, i * 1.5, -6, 0.2);
else if (obj.counter == 170) {
obj.counter = 0;
obj.animSpeed = 0; //start animating again
}
}
}
void Tree(jjOBJ@ obj) {
if (jjLowDetail && !obj.deactivates)
return;
if (obj.state == STATE::START) {
const uint xTile = uint(obj.xOrg) >> 5, yTile = uint(obj.yOrg) >> 5;
obj.frameID = jjParameterGet(xTile,yTile, 0, 3);
obj.curFrame = jjAnimations[obj.curAnim] + obj.frameID;
obj.var[0] = int(jjParameterGet(xTile,yTile, 3, -5) * -8.53333333); //angle
obj.var[1] = jjParameterGet(xTile,yTile, 8, -3) + 5; //layer... shouldn't be -4 in this level, but I guess I don't want to show a warning message?
obj.deactivates = obj.var[1] == 5 || obj.var[1] == 6;
obj.state = STATE::IDLE;
} else if (obj.state == STATE::DEACTIVATE) {
obj.deactivate();
} else {
const auto spriteMode = obj.var[1] <= 6 ? SPRITE::NORMAL : SPRITE::PALSHIFT;
if (obj.frameID == 5) { //bendy tree
if (!jjLowDetail) {
uint trunkPieceCount = 0;
float xPos = obj.xOrg, yPos = obj.yOrg;
uint curFrame = obj.curFrame;
float angle = obj.var[0];
const float angleChange = jjSin(jjGameTicks << 1) * 3;
while (true) {
if (trunkPieceCount == 15)
curFrame += 1;
jjDrawRotatedSpriteFromCurFrame(xPos,yPos, curFrame, int(angle), 1,1, spriteMode,16, obj.var[1],obj.var[1]-1);
if (trunkPieceCount == 15)
break;
++trunkPieceCount;
angle += angleChange;
xPos -= jjSin(int(angle)) * 5;
yPos -= jjCos(int(angle)) * 5;
}
}
} else { //single sprite
if (obj.frameID >= 3 && jjGameTicks % 35 == 0) //sunflower
obj.curFrame = jjAnimations[obj.curAnim] + (obj.frameID ^= 7); //animate... ^=7 switches between 3 and 4.
jjDrawRotatedSpriteFromCurFrame(obj.xOrg, obj.yOrg, obj.curFrame, obj.var[0], 1,1, spriteMode,16, obj.var[1],obj.var[1]-1);
}
}
}
void MakeTreeObjects() {
for (int x = jjLayerWidth[4]; --x >= 0;)
for (int y = jjLayerWidth[4]; --y >= 0;)
if (jjEventGet(x,y) == OBJECT::CHERRY) {
jjParameterSet(x,y, -1,1, 1);
jjAddObject(OBJECT::CHERRY, x*32 + 15, y*32 + 15, 0, CREATOR::LEVEL);
}
}
void onLevelReload() {
MakeTreeObjects();
}
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);
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 && 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) {
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]
);
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;
}
}
/*bool onDrawScore(jjPLAYER@, jjCANVAS@ canvas) {
for (int x = jjResolutionWidth; --x >= 0;)
for (int y = jjResolutionHeight; --y >= 0;)
canvas.drawPixel(x,y, x^y);
return false;
}*/
void PlatformWrapper(jjOBJ@ obj) {
obj.behave(BEHAVIOR::PLATFORM, true);
jjDrawSpriteFromCurFrame(obj.xOrg, obj.yOrg, obj.curFrame+2);
}
void GrassPlatform(jjOBJ@ obj) {
if (obj.state == STATE::DEACTIVATE)
obj.deactivate();
else if (obj.state == STATE::START) {
obj.state = STATE::FLY;
obj.var[0] = jjParameterGet(uint(obj.xOrg) >> 5,uint(obj.yOrg) >> 5, 2,3);
} else {
const auto lastY = obj.yPos;
obj.yPos = obj.yOrg + jjSin(obj.age += 4) * 6;
const int stage = obj.var[0] == 0 ? 256 : ((obj.var[0] * 256 + jjGameTicks * 8) & 2047);
const uint8 opacity = stage < 256 ? stage : stage > 1280 ? 0 : stage > 1024 ? (1280 - stage) : 255;
bool stoodOn = false;
if (opacity < 100)
obj.clearPlatform();
else {
obj.bePlatform(obj.xPos, lastY);
for (int i = 0; i < jjLocalPlayerCount; ++i)
if (jjLocalPlayers[i].platform == obj.objectID) {
stoodOn = true;
break;
}
}
if (stoodOn) {
if (obj.counterEnd < 10) obj.counterEnd += 2;
} else {
if (obj.counterEnd != 0) obj.counterEnd -= 1;
}
obj.yPos += obj.counterEnd;
jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame,1, SPRITE::BLEND_NORMAL, opacity);
}
}
class Ball : WalkingEnemy {
float lastBallX, lastBallY; //make these globalish so they persist between onBehave and onDraw, meaning the ball can be drawn exactly underneath the player (while falling) instead of one frame ahead.
Ball(jjOBJ@ preset) {
preset.determineCurAnim(ANIM::CUSTOM[AnimSets::BALL], 0);
preset.energy = 5;
preset.points = 0;
preset.xSpeed = 1;
super(preset, 12);
preset.playerHandling = HANDLING::SPECIAL;
preset.scriptedCollisions = true;
preset.bulletHandling = HANDLING::DETECTBULLET;
preset.yAcc = 0.1;
}
void onBehave(jjOBJ@ obj) override {
if (obj.state == STATE::KILL) {
obj.clearPlatform();
obj.delete(); //no turtle
} else {
lastBallX = obj.xPos;
lastBallY = obj.yPos;
WalkingEnemy::onBehave(obj);
obj.bePlatform(lastBallX, lastBallY);
}
}
void onDraw( jjOBJ@ obj) override {
jjDrawSpriteFromCurFrame(lastBallX,lastBallY, obj.curFrame, 1, (obj.justHit == 0) ? (obj.state != STATE::FREEZE) ? SPRITE::NORMAL : SPRITE::FROZEN : SPRITE::SINGLECOLOR, 15); //don't rotate, don't mirror
}
bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) override {
obj.scriptedCollisions = false;
if (bullet !is null) {
obj.bulletHandling = HANDLING::HURTBYBULLET;
bullet.objectHit(obj, HANDLING::ENEMY);
obj.bulletHandling = HANDLING::DETECTBULLET;
} else {
if (play.yPos > obj.yPos && ((play.xPos > obj.xPos) == (obj.direction == 1)))
play.objectHit(obj, force, HANDLING::ENEMY);
}
obj.scriptedCollisions = true;
return true;
}
}
class Newtron : Enemy {
Newtron(jjOBJ@ preset) {
preset.determineCurAnim(ANIM::CUSTOM[AnimSets::NEWTRON], 0);
preset.energy = 1;
preset.points = 200;
super(preset);
preset.playerHandling = HANDLING::PARTICLE;
}
void onBehave(jjOBJ@ obj) {
switch (obj.state) {
case STATE::START:
obj.state = STATE::WAIT;
{
const uint xTile = uint(obj.xPos) >> 5, yTile = uint(obj.yPos) >> 5;
obj.var[0] = jjTileGet(4, xTile,yTile);
if (obj.var[0] == 0) obj.var[0] = jjTileGet(5, xTile,yTile);
}
break;
case STATE::KILL:
spawnTurtlette(obj);
for (int i = 0; i < 7; ++i)
jjObjects[jjAddObject(OBJECT::SHARD, obj.xPos, obj.yPos)].determineCurAnim(ANIM::CUSTOM[AnimSets::NEWTRON], 3);
obj.delete();
break;
case STATE::DEACTIVATE:
obj.deactivate();
break;
case STATE::FREEZE:
if (--obj.freeze == 0)
obj.state = obj.oldState;
break;
case STATE::WAIT: {
if (findNearbyPlayer(obj)) {
if (obj.counterEnd == 0) {
jjSample(obj.xPos, obj.yPos, SOUND::COMMON_COLLAPS, 63, 10000);
if (obj.var[0] != 0)
jjAddParticleTileExplosion(uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5, obj.var[0], false);
}
if ((obj.counterEnd += 6) >= 200) {
obj.playerHandling = HANDLING::ENEMY;
obj.counterEnd = 252;
obj.state = STATE::ACTION;
}
} else if (obj.counterEnd > 0)
obj.counterEnd -= 6;
}
break;
case STATE::ACTION:
action(obj);
break;
}
}
bool findNearbyPlayer(jjOBJ@ obj) {
const auto nearestPlayerID = obj.findNearestPlayer(160*160);
if (nearestPlayerID >= 0) {
obj.direction = (jjPlayers[nearestPlayerID].xPos > obj.xPos) ? 1 : -1;
return true;
}
return false;
}
void action(jjOBJ@ obj) {
}
void onDraw(jjOBJ@ obj) {
int curFrame = obj.curFrame;
if (obj.frameID == 0)
curFrame += (jjGameTicks >> 1) & 7;
if (obj.playerHandling == HANDLING::PARTICLE)
jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, curFrame, obj.direction, SPRITE::BLEND_NORMAL, obj.counterEnd);
else
jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, curFrame, obj.direction, (obj.justHit == 0) ? (obj.state != STATE::FREEZE) ? SPRITE::NORMAL : SPRITE::FROZEN : SPRITE::SINGLECOLOR, 15);
}
}
class GreenNewtron : Newtron {
GreenNewtron(jjOBJ@ preset) {
super(preset);
}
void action(jjOBJ@ obj) override {
if (obj.counterEnd-- == 252)
AddEnemyBullet(obj, obj.direction * (2 + (jjRandom() & 7) / 10.0), 0,0);
else if (obj.counterEnd == 204) {
if (findNearbyPlayer(obj))
obj.counterEnd = 252;
else {
obj.state = STATE::WAIT;
obj.playerHandling = HANDLING::PARTICLE;
}
}
}
}
class BlueNewtron : Newtron {
BlueNewtron(jjOBJ@ preset) {
super(preset);
preset.curFrame += 20;
preset.curAnim += 2;
preset.frameID = 8;
}
void action(jjOBJ@ obj) override {
if (obj.counterEnd == 252) {
obj.frameID = 0;
obj.curFrame -= 8;
obj.counterEnd = 251;
} else if (obj.counterEnd == 251) {
if (jjMaskedPixel(int(obj.xPos), int(obj.yPos) + 8)) {
obj.counterEnd = 0;
jjSample(obj.xPos, obj.yPos, SOUND::COMMON_SWISH1);
} else
obj.yPos += 4;
} else {
obj.xPos += obj.direction * 3;
}
}
}
class Chopper : Enemy {
Chopper(jjOBJ@ preset) {
preset.determineCurAnim(ANIM::CUSTOM[AnimSets::CHOPPER], 0);
preset.energy = 2;
preset.points = 500;
super(preset);
}
void onBehave(jjOBJ@ obj) {
switch (obj.state) {
case STATE::START:
obj.state = STATE::BOUNCE;
obj.var[0] = jjTileGet(4, uint(obj.xOrg) >> 5, uint(obj.yOrg) >> 5);
break;
case STATE::KILL:
spawnTurtlette(obj);
obj.delete();
break;
case STATE::DEACTIVATE:
obj.deactivate();
break;
case STATE::FREEZE:
if (--obj.freeze == 0)
obj.state = obj.oldState;
break;
case STATE::BOUNCE:
obj.curFrame = jjAnimations[obj.curAnim] + ((++obj.age >> 3) & 3);
if (obj.special == 0) {
for (int i = 0; i < jjLocalPlayerCount; ++i) {
if (jjLocalPlayers[i].yPos < obj.yPos) {
const auto xDelta = obj.xPos - jjLocalPlayers[i].xPos;
if (abs(xDelta) < 128) {
obj.direction = xDelta > 0 ? 1 : -1;
if (jjTileGet(4, uint(obj.xPos - 32 * obj.direction) >> 5, uint(obj.yOrg) >> 5) != uint(obj.var[0]))
obj.direction = -obj.direction;
obj.special = 4;
jjSample(obj.xPos, obj.yPos, SOUND::COMMON_WATER);
}
}
}
} else if ((obj.special += 4) == 512) {
obj.special = 0;
} else {
obj.yPos = obj.yOrg - jjSin(obj.special) * 192;
obj.xPos -= obj.direction / 4.f;
}
break;
}
}
void onDraw(jjOBJ@ obj) {
jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.special * obj.direction, obj.direction,1, (obj.justHit == 0) ? (obj.state != STATE::FREEZE) ? SPRITE::NORMAL : SPRITE::FROZEN : SPRITE::SINGLECOLOR, 15);
}
}
class BuzzBomber : Enemy {
BuzzBomber(jjOBJ@ preset) {
preset.determineCurAnim(ANIM::CUSTOM[AnimSets::BUZZBOMBER], 0);
preset.energy = 1;
preset.points = 1000;
preset.state = STATE::IDLE; //no particular setup needed
super(preset);
}
void onBehave(jjOBJ@ obj) {
obj.age += 4;
switch (obj.state) {
case STATE::KILL:
spawnTurtlette(obj);
obj.delete();
break;
case STATE::DEACTIVATE:
obj.deactivate();
break;
case STATE::FREEZE:
if (--obj.freeze == 0)
obj.state = obj.oldState;
break;
case STATE::IDLE:
obj.var[0] = obj.findNearestPlayer(200*200);
if (obj.var[0] >= 0) {
obj.state = STATE::FLY;
obj.xSpeed = 0.75 * obj.direction; //some deaccel maybe
} else {
obj.yPos = obj.yOrg + jjSin(obj.age) * 8;
if (jjMaskedVLine(int(obj.xPos + obj.direction * 12), int(obj.yPos) - 8, 16))
obj.direction = -obj.direction;
else
obj.xPos += 0.75 * obj.direction;
break;
}
case STATE::FLY: {
const jjPLAYER@ target = jjPlayers[obj.var[0]]; //once a target has been found, never abandon it (short of deactivation), even if there are multiple local players
obj.direction = (target.xPos > obj.xPos) ? 1 : -1;
const int distance = 150 + int(jjSin(obj.age) * 25);
const float targetX = target.xPos - distance * obj.direction, targetY = target.yPos - distance;
if (obj.xPos < targetX) {
if (obj.xSpeed < 0.8) obj.xSpeed += 0.075;
} else {
if (obj.xSpeed > -0.8) obj.xSpeed -= 0.075;
}
if (obj.yPos < targetY) {
if (obj.ySpeed < 0.8) obj.ySpeed += 0.075;
} else {
if (obj.ySpeed > -0.8) obj.ySpeed -= 0.075;
}
obj.ySpeed += (int(jjRandom() & 63) - 31) / 310.f;
obj.xPos += obj.xSpeed;
obj.yPos += obj.ySpeed;
if (++obj.counter > 70 && abs(abs(target.xPos - obj.xPos) - (target.yPos - obj.yPos)) < 40) { //a poor man's atan
obj.curFrame = jjAnimations[obj.curAnim] + 2;
obj.state = STATE::ATTACK;
obj.counter = 40;
}
break; }
case STATE::ATTACK:
if (--obj.counter == 15)
AddEnemyBullet(obj, obj.direction * 2, 2, 0);
else if (obj.counter == 0)
obj.state = STATE::FLY;
}
if (obj.state != STATE::ATTACK && obj.state != STATE::FREEZE) {
obj.curFrame = jjAnimations[obj.curAnim] + ((obj.age >> 4) & 1);
obj.counterEnd = jjSampleLooped(obj.xPos, obj.yPos, SOUND::DRAGFLY_BEELOOP, obj.counterEnd);
}
}
void onDraw(jjOBJ@ obj) {
jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, (obj.state != STATE::FREEZE) ? (obj.justHit == 0) ? SPRITE::NORMAL : SPRITE::SINGLECOLOR : SPRITE::FROZEN,15, 3);
}
}
enum PlayerState { Normal, Loop, Ropeway };
enum LoopAngle { Floor, WallOnLeft, Ceiling, WallOnRight };
class PlayerX {
PlayerX(){}
PlayerState State = PlayerState::Normal;
LoopAngle CurrentAngle = LoopAngle::Floor, NextAngle;
float CornerXOrg, CornerYOrg, CornerSpeed, CornerElapsed = -1,/* CornerLength,*/ LastX, LastY;
//int LastLoopEventParameter = -1;
int LastTile;
int CornerAngleChange, CornerRepositioningMultiplier, CornerAngleMultiplier;
int LastHealth = -1;
int LastGems = 0;
}
array<PlayerX> PlayerXs(jjLocalPlayerCount);
class CornerEventResult {
LoopAngle Angle;
int XOrg, YOrg;
CornerEventResult(LoopAngle a, int x, int y) { Angle = a; XOrg = x; YOrg = y; }
}
const array<const CornerEventResult@> NextLoopAngles = {
null, CornerEventResult(WallOnRight, 0, -1), //floor, turn left, loop
null, CornerEventResult(Floor, 2, 0), //wallonleft, turn left, loop
null, CornerEventResult(WallOnLeft, 1, 2), //ceiling, turn left, loop
null, CornerEventResult(Ceiling, -1, 1), //wallonright, turn left, loop
CornerEventResult(WallOnLeft, 1, -1), null, //floor, turn right, loop
CornerEventResult(Ceiling, 2, 1), null, //wallonleft, turn right, loop
CornerEventResult(WallOnRight, 0, 2), null, //ceiling, turn right, loop
CornerEventResult(Floor, -1, 0), null, //wallonright, turn right, loop
CornerEventResult(WallOnRight, 1, 4), null, //floor, turn left, corner
CornerEventResult(Floor, -3, 1), null, //wallonleft, turn left, corner
CornerEventResult(WallOnLeft, 0, -3), null, //ceiling, turn left, corner
CornerEventResult(Ceiling, 4, 0), null, //wallonright, turn left, corner
null, CornerEventResult(WallOnLeft, 0, 4), //floor, turn right, corner
null, CornerEventResult(Ceiling, -3, 0), //wallonleft, turn right, corner
null, CornerEventResult(WallOnRight, 1, -3), //ceiling, turn right, corner
null, CornerEventResult(Floor, 4, 1) //wallonright, turn right, corner
};
void onPlayer(jjPLAYER@ play) {
PlayerX@ playX = PlayerXs[play.localPlayerID];
if (playX.LastHealth != play.health) {
if (playX.LastHealth > play.health) { //injured
playX.State = PlayerState::Normal;
playX.CurrentAngle = LoopAngle::Floor;
playX.LastTile = -1;
play.invisibility = false;
play.cameraUnfreeze();
const int maxGemsLost = 25 - (jjDifficulty * 5); //25, 20, 15, 10
if (play.gems[GEM::RED] == 0) {
if (play.health > 0) {
play.health = 0;
play.lives -= 1;
}
} else for (int i = 0; i < maxGemsLost && i < play.gems[GEM::RED]; ++i) {
jjOBJ@ gem = jjObjects[jjAddObject(OBJECT::FLICKERGEM, play.xPos, play.yPos, play.playerID, CREATOR::PLAYER)];
uint rand = jjRandom();
gem.xSpeed = (int(rand & 15) - 7);
gem.direction = int((rand >>= 4) & 1) * 2 - 1;
gem.ySpeed = 0.025 - int((rand >> 1) & 7);
gem.playerHandling = HANDLING::DELAYEDPICKUP;
gem.var[2] = 100; //time spent unpickupable
gem.counter /= 3; //time till death
}
play.gems[GEM::RED] = 0;
}
if (playX.LastHealth <= 0) //respawning at checkpoint
play.gems[GEM::RED] = 0;
playX.LastHealth = play.health;
return;
} else if (playX.LastGems / 100 < play.gems[GEM::RED] / 100) {
play.lives += 1;
jjSamplePriority(SOUND::COMMON_HARP1);
}
playX.LastGems = play.gems[GEM::RED];
if (playX.State == PlayerState::Ropeway) {
play.keyLeft = play.keyRight = play.keyRun = play.keyDown = false;
play.xSpeed = play.ySpeed = 0;
//play.invincibility = -2;
return;
}
const uint xTile = uint(play.xPos) >> 5, yTile = uint(play.yPos) >> 5;
const uint8 currEvent = jjEventGet(xTile,yTile);
if (currEvent == CornerEventID) {
const int loopEventParameter = jjParameterGet(xTile, yTile, 0, 4);
if ((loopEventParameter >> 2) == playX.CurrentAngle) {
const auto curAnim = play.curAnim - jjAnimSets[play.setID];
if (play.currTile != playX.LastTile && (playX.State == PlayerState::Loop || (curAnim >= RABBIT::RUN1 && curAnim <= RABBIT::RUN3))) { //running
//if (loopEventParameter != playX.LastLoopEventParameter) {
const int circumstance = ((loopEventParameter & 3) << 3) | (playX.CurrentAngle << 1) | (play.direction == 1 ? 1 : 0);
const CornerEventResult@ result = NextLoopAngles[circumstance];
if (result !is null) {
playX.LastTile = play.currTile;
//playX.LastLoopEventParameter = loopEventParameter;
//jjAlert(result.Angle + "," + result.XOrg + "," + result.YOrg);
playX.NextAngle = result.Angle;
playX.CornerXOrg = (int(xTile) + result.XOrg) * 32;
playX.CornerYOrg = (int(yTile) + result.YOrg) * 32;
const bool isLoop = loopEventParameter & 2 == 0;
//playX.CornerLength = isLoop ? 256 : 256; //todo
playX.CornerElapsed = 0; //todo?
playX.CornerRepositioningMultiplier = isLoop ? 44 : 120; //40:120
playX.CornerAngleMultiplier = isLoop ? 1 : -1;
playX.CornerSpeed = isLoop ? 30 : 18;
playX.CornerAngleChange = int(playX.NextAngle - playX.CurrentAngle);
if (abs(playX.CornerAngleChange) == 3)
playX.CornerAngleChange /= -3;
playX.State = PlayerState::Loop;
play.invisibility = true;
//play.noclipMode = true;
play.xSpeed = play.ySpeed = 0;
if (jjParameterGet(xTile, yTile, -2, 1) == 1)
play.cameraFreeze(play.cameraX, play.cameraY, false, true);
}
//}
}
}
} else if (currEvent == AREA::FLYOFF) { //this feels like the sensible choice
if (playX.State == PlayerState::Loop)
play.keyJump = true;
} else
playX.LastTile = -1;
int angle = playX.CurrentAngle << 8;
const int speedDivider = play.keyRun ? 1 : 4;
if (playX.CornerElapsed >= 0) {
playX.CornerElapsed += playX.CornerSpeed / speedDivider;
if (playX.CornerElapsed > 256)
playX.CornerElapsed = 256;
angle += int(playX.CornerElapsed * playX.CornerAngleChange);
play.xPos = playX.LastX = playX.CornerXOrg + jjSin(angle) * playX.CornerRepositioningMultiplier * -playX.CornerAngleMultiplier;
play.yPos = playX.LastY = playX.CornerYOrg - jjCos(angle) * playX.CornerRepositioningMultiplier * -playX.CornerAngleMultiplier;
if (playX.CornerElapsed == 256) {
playX.CornerElapsed = -1;
playX.CurrentAngle = playX.NextAngle;
}
} else if (playX.State == PlayerState::Loop) {
const int speedMultiplier = 16 / speedDivider * play.direction;
play.xPos = playX.LastX += jjCos(angle) * speedMultiplier;
play.yPos = playX.LastY += jjSin(angle) * speedMultiplier;
}
if (playX.State == PlayerState::Loop) {
if (playX.CornerAngleMultiplier == 1 && playX.CornerElapsed >= 0) //quarter pipe
play.keyJump = false;
if ((playX.CornerElapsed < 0 && playX.CurrentAngle == LoopAngle::Floor) || play.keyJump || (play.direction == 1 && !play.keyRight) || (play.direction == -1 && !play.keyLeft)) {
playX.State = PlayerState::Normal;
playX.CurrentAngle = LoopAngle::Floor;
playX.CornerElapsed = -1;
play.invisibility = false;
//play.noclipMode = false;
play.xSpeed = play.direction * jjCos(angle) * 16 / speedDivider;
play.ySpeed = play.direction * jjSin(angle) * 16 / speedDivider;
play.cameraUnfreeze();
} else {
play.keyLeft = play.keyRight = play.keyFire = false;
play.xSpeed = play.ySpeed = 0;
jjDrawRotatedSprite(play.xPos, play.yPos, play.setID, play.keyRun ? RABBIT::RUN3 : RABBIT::RUN1, jjGameTicks >> 2, -angle, play.direction,1, SPRITE::PLAYER, play.playerID);
}
}
}
void LoopObject(jjOBJ@ obj) {
if (obj.state == STATE::START) {
obj.state = STATE::ROTATE;
obj.var[0] = jjParameterGet(uint(obj.xOrg) >> 5,uint(obj.yOrg) >> 5, 0,1);
obj.var[1] = jjParameterGet(uint(obj.xOrg) >> 5,uint(obj.yOrg) >> 5, 1,4);
obj.doesHurt = 0; //starts at 0, so (LoopsUnmaskedOnLeft != leftSideUnmasked) below will always be true the first time
} if (obj.state == STATE::DEACTIVATE)
obj.deactivate();
else {
const auto nearestPlayerID = obj.findNearestPlayer(960*960);
if (nearestPlayerID >= 0) {
const jjPLAYER@ play = jjPlayers[nearestPlayerID];
const uint spacing = obj.var[1];
uint8 leftSideUnmasked;
if (play.xPos < obj.xOrg - 32)
leftSideUnmasked = 2;
else if (play.xPos > obj.xOrg + spacing*32 + 128)
leftSideUnmasked = 1;
else if (PlayerXs[play.localPlayerID].CurrentAngle == LoopAngle::Ceiling)
leftSideUnmasked = play.direction == -1 ? 2 : 1;
else
return;
if (obj.doesHurt != leftSideUnmasked) {
obj.doesHurt = leftSideUnmasked;
const uint xTile = uint(obj.xOrg) >> 5, yTile = uint(obj.yOrg) >> 5;
const bool grassOnRight = obj.var[0] == 1;
if (leftSideUnmasked == 2) {
jjTileSet(4, xTile + 0, yTile, 0);
jjTileSet(4, xTile + 1, yTile, 0);
jjTileSet(4, xTile + spacing + 2, yTile, grassOnRight ? 323 : 306);
jjTileSet(4, xTile + spacing + 3, yTile, grassOnRight ? 324 : 307);
jjTileSet(4, xTile + 0, yTile - 1, 0);
jjTileSet(4, xTile + spacing + 3, yTile - 1, 297);
jjEventSet(xTile + 1, yTile, 0);
jjEventSet(xTile + spacing + 2, yTile, CornerEventID);
} else {
jjTileSet(4, xTile + 0, yTile, grassOnRight ? 304 : 325);
jjTileSet(4, xTile + 1, yTile, grassOnRight ? 305 : 326);
jjTileSet(4, xTile + spacing + 2, yTile, 0);
jjTileSet(4, xTile + spacing + 3, yTile, 0);
jjTileSet(4, xTile + 0, yTile - 1, 294);
jjTileSet(4, xTile + spacing + 3, yTile - 1, 0);
jjEventSet(xTile + 1, yTile, CornerEventID);
jjEventSet(xTile + spacing + 2, yTile, 0);
}
}
}
}
}
/*void onMain() {
for (int xTile = jjLayerWidth[4]; --xTile >= 0;)
for (int yTile = jjLayerHeight[4]; --yTile >= 0;)
if (jjEventGet(xTile,yTile) == CornerEventID)
jjDrawString(xTile*32 + 10, yTile*32 + 10, "" + jjParameterGet(xTile,yTile, 0,4));
}*/
void AddCornerEvents() {
for (int xTile = jjLayerWidth[4]; --xTile >= 0;)
for (int yTile = jjLayerHeight[4]; --yTile >= 0;) {
LoopAngle inputAngle = LoopAngle::Floor;
bool turnRight = false;
bool isLoop = false;
int xOffset = 0, yOffset = 0;
switch (jjTileGet(4, xTile, yTile)) {
case 262:
case 328:
case 354:
yOffset = -1;
break;
case 280:
case 372:
xOffset = -1;
turnRight = true;
inputAngle = LoopAngle::WallOnRight;
break;
case 272:
xOffset = 1;
turnRight = true;
inputAngle = LoopAngle::WallOnLeft;
break;
case 290:
yOffset = 1;
inputAngle = LoopAngle::Ceiling;
break;
case 300:
case 337:
case 355:
yOffset = -1;
turnRight = true;
break;
case 322:
case 377:
xOffset = 1;
inputAngle = LoopAngle::WallOnLeft;
break;
case 310:
xOffset = -1;
inputAngle = LoopAngle::WallOnRight;
break;
case 332:
yOffset = 1;
turnRight = true;
inputAngle = LoopAngle::Ceiling;
break;
case 276:
isLoop = true;
turnRight = true;
inputAngle = LoopAngle::Ceiling;
break;
case 287:
isLoop = true;
inputAngle = LoopAngle::WallOnRight;
break;
case 297:
case 351:
isLoop = true;
turnRight = true;
inputAngle = LoopAngle::WallOnRight;
break;
case 306:
case 323:
case 360:
isLoop = true;
break;
case 305:
case 326:
case 369:
isLoop = true;
turnRight = true;
break;
case 294:
case 358:
isLoop = true;
inputAngle = LoopAngle::WallOnLeft;
break;
case 284:
isLoop = true;
turnRight = true;
inputAngle = LoopAngle::WallOnLeft;
break;
case 275:
isLoop = true;
inputAngle = LoopAngle::Ceiling;
break;
default:
continue;
}
if (jjEventGet(xTile + xOffset, yTile + yOffset) == 0) {
jjEventSet(xTile + xOffset, yTile + yOffset, CornerEventID);
jjParameterSet(xTile + xOffset, yTile + yOffset, 0,4, (turnRight ? 1 : 0) | (isLoop ? 0 : 2) | (inputAngle << 2));
}
}
}
class Ropeway : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) {
jjPLAYER@ play = jjPlayers[obj.var[0]];
bool isRealPlayer = obj.var[0] >= 0 && PlayerXs[play.localPlayerID].State == PlayerState::Ropeway;
if (obj.state == STATE::START) {
obj.state = STATE::WAIT;
obj.var[0] = -1; //no player yet
const uint xTile = uint(obj.xOrg) >> 5, yTile = uint(obj.yOrg) >> 5;
obj.direction = jjParameterGet(xTile,yTile, 0,1) * 2 - 1;
obj.special = jjParameterGet(xTile,yTile, 1,1); //rotates
} else if (obj.state == STATE::DEACTIVATE) {
reset(obj);
} else if (obj.state == STATE::ROTATE) {
obj.special += int(obj.xAcc * 3 * obj.direction);
const float sin = jjSin(obj.special), cos = jjCos(obj.special);
jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos - 24, obj.curFrame, obj.special);
if (isRealPlayer) {
jjDrawRotatedSprite(play.xPos = (obj.xPos + sin * 30), play.yPos = (obj.yPos + cos * 30 - 24), play.setID, RABBIT::HANGIDLE2, jjGameTicks>>3, obj.special, play.direction = obj.direction,1, SPRITE::PLAYER, obj.var[0]);
if (play.keyJump) {
release(obj);
obj.counter = 200;
}
obj.counterEnd = jjSampleLooped(obj.xPos, obj.yPos, SOUND::COMMON_FOEW5, obj.counterEnd, int(abs(cos) * 46) + 15);
} else
release(obj);
} else {
jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos - 24, obj.curFrame);
if (obj.state == STATE::ACTION) {
if (isRealPlayer) {
jjDrawSprite(play.xPos = obj.xPos, (play.yPos = obj.yPos) + 6, play.setID, play.keyFire ? RABBIT::HANGINGFIRERIGHT : RABBIT::HANGIDLE2, jjGameTicks>>3, play.direction = obj.direction, SPRITE::PLAYER, obj.var[0]);
if (play.keyJump && obj.xAcc > 3.f) {
release(obj);
play.ySpeed = -5;
obj.state = STATE::ACTION;
}
}
if (obj.xAcc < 8.f)
obj.xAcc += 0.1f;
if (jjGameTicks & 1 == 0) {
jjPARTICLE@ part = jjAddParticle(PARTICLE::SPARK);
if (part !is null) {
part.xPos = obj.xPos - obj.direction*7;
part.yPos = obj.yPos - 21;
part.xSpeed = -obj.direction;
part.ySpeed = 0.5;
}
}
obj.xPos += obj.xAcc * obj.direction;
obj.yPos += obj.xAcc / 2;
obj.counterEnd = jjSampleLooped(obj.xPos, obj.yPos, SOUND::COMMON_FOEW5, obj.counterEnd);
const auto newTileID = jjTileGet(4, uint(obj.xPos)>>5, uint(obj.yPos)>>5);
if (newTileID == 496 || newTileID == 498) { //side of a tree
obj.xPos += obj.xAcc * obj.direction * 2;
obj.yPos += obj.xAcc;
if (obj.special == 1 && isRealPlayer)
obj.state = STATE::ROTATE;
else
release(obj);
}
} else if (obj.state == STATE::DONE) {
if (++obj.counter > 210) {
obj.particlePixelExplosion(0);
reset(obj);
}
}
}
}
void reset(jjOBJ@ obj) const {
obj.state = STATE::START;
obj.xPos = obj.xOrg;
obj.yPos = obj.yOrg;
obj.playerHandling = HANDLING::PICKUP;
obj.counter = 0;
}
void release(jjOBJ@ obj) const {
if (obj.var[0] >= 0) {
jjPLAYER@ play = jjPlayers[obj.var[0]];
PlayerX@ playX = PlayerXs[play.localPlayerID];
if (playX.State == PlayerState::Ropeway) {
playX.State = PlayerState::Normal;
const float xThrust = obj.xAcc * obj.direction * 2;
play.xAcc = jjCos(obj.special) * xThrust;
play.ySpeed = -jjSin(obj.special) * xThrust;
play.direction = obj.direction;
play.invisibility = false;
}
}
obj.state = STATE::DONE;
}
bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ play, int force) {
PlayerX@ playX = PlayerXs[play.localPlayerID];
if (obj.var[0] == -1 && playX.State == PlayerState::Normal) {
obj.var[0] = play.playerID;
obj.state = STATE::ACTION;
obj.playerHandling = HANDLING::PARTICLE; //no further collisions
obj.xAcc = 0;
playX.State = PlayerState::Ropeway;
play.invisibility = true;
}
return true;
}
}
class Turtlette : WalkingEnemy {
Turtlette(jjOBJ@ preset) {
preset.determineCurAnim(ANIM::CUSTOM[AnimSets::TURTLETTE], 0);
preset.xSpeed = 2.5;
preset.ySpeed = -4;
preset.yAcc = 0.3;
preset.deactivates = false;
super(preset, 6);
preset.playerHandling = HANDLING::SPECIALDONE;
preset.isTarget = false;
preset.state = STATE::FALL;
}
void onBehave(jjOBJ@ obj) override {
if (obj.state == STATE::START && obj.creatorType == CREATOR::OBJECT)
obj.direction = jjObjects[obj.creatorID].direction;
if (obj.state == STATE::WALK)
obj.yAcc = 0;
WalkingEnemy::onBehave(obj);
}
}
bool onDrawHealth(jjPLAYER@ player, jjCANVAS@ canvas) {
return player.gems[GEM::RED] == 0 && jjRenderFrame & 3 < 2;
}
void InvisibleSpring(jjOBJ@ obj) { obj.behave(BEHAVIOR::SPRING, false); }
uint ChainAnim, BallAnim;
const jjANIMFRAME@ BallCollisionFrame;
class BossBall {
int xPos = 0, yPos = 0;
float scale = 1;
void method(const jjOBJ@ obj, bool draw) const {
float chainX = obj.xPos, chainY = obj.yPos;
const float deltaX = xPos - chainX, deltaY = yPos - chainY;
const auto angle = atan2(deltaY, deltaX);
int stepCount = int(sqrt(deltaY*deltaY + deltaX*deltaX) / 10);
const float stepX = cos(angle) * 10, stepY = sin(angle) * 10;
if (draw) {
const auto ChainFrame = jjAnimations[ChainAnim].firstFrame;
const auto BallFrame = jjAnimations[BallAnim].firstFrame;
while (stepCount-- > 0)
jjDrawResizedSpriteFromCurFrame(chainX += stepX, chainY += stepY, ChainFrame, scale,scale);
jjDrawResizedSpriteFromCurFrame(xPos, yPos - 26, BallFrame + obj.frameID, scale,scale, SPRITE::NORMAL,0, scale < 1.f ? 4 : 3);
} else {
while (stepCount-- > 0)
jjObjects[jjAddObject(OBJECT::SHARD, chainX += stepX, chainY += stepY)].curAnim = ChainAnim;
jjObjects[jjAddObject(OBJECT::SHARD, xPos, yPos - 26)].curAnim = BallAnim;
}
}
}
array<BossBall> BossBalls;
class Boss : Enemy {
Boss(jjOBJ@ preset) {
preset.determineCurAnim(ANIM::CUSTOM[AnimSets::BOSS], 0);
preset.energy = 100;
super(preset, 1);
preset.deactivates = false;
ChainAnim = jjObjectPresets[OBJECT::PINKPLATFORM].curAnim + 1;
BallAnim = jjObjectPresets[OBJECT::BURGER].curAnim;
@BallCollisionFrame = jjAnimFrames[jjAnimations[BallAnim]];
}
void onBehave(jjOBJ@ obj) {
switch (obj.state) {
case STATE::START:
for (int i = 0; i < jjLocalPlayerCount; ++i) {
jjPLAYER@ localPlayer = jjLocalPlayers[i];
if (localPlayer.bossActivated) {
localPlayer.boss = obj.objectID;
obj.state = STATE::BOUNCE;
obj.age = 256;
BossBalls.resize(0);
BossBalls.insertLast(BossBall());
BossBalls[0].xPos = int(obj.xPos);
BossBalls[0].yPos = int(obj.yPos);
break;
}
}
return;
case STATE::FREEZE:
if (--obj.freeze == 0)
obj.state = obj.oldState;
break;
case STATE::BOUNCE:
if (obj.counter < 180) ++obj.counter;
obj.age += 2;
obj.xPos = obj.xOrg + jjSin(obj.age) * 140;
obj.yPos = obj.yOrg + jjSin(obj.age << 2) * 5;
obj.direction = ((obj.age + 256) & 1023) < 511 ? 1 : -1;
obj.frameID = (obj.age >> 4) & 3; //ball
{
BossBall@ ball = BossBalls[0];
ball.xPos = int(obj.xPos + jjSin(obj.age) * obj.counter);
ball.yPos = int(obj.yPos + abs(jjCos(obj.age)) * obj.counter * 5 / 6);
}
if (obj.age & 511 == 256 && obj.energy <= 60)
obj.state = STATE::EXTRA;
break;
case STATE::EXTRA:
if (++obj.counterEnd < 80) { //shake
obj.xPos += int(jjRandom() & 3) - 1.5;
obj.yPos += int(jjRandom() & 3) - 1.5;
} else if (obj.counterEnd == 80) {
jjAddParticlePixelExplosion(obj.xPos, obj.yPos, obj.curFrame + 2, obj.direction, 2); //screen explodes
jjSamplePriority(SOUND::COMMON_ICECRUSH);
obj.state = STATE::ATTACK;
}
break;
case STATE::ATTACK: {
float targetX, targetY;
const auto nearestPlayerID = obj.findNearestPlayer(400*400);
if (nearestPlayerID >= 0) {
const jjPLAYER@ play = jjPlayers[nearestPlayerID];
targetX = play.xPos;
targetY = play.yPos;
} else {
targetX = obj.xOrg;
targetY = obj.yOrg;
}
const float maxXSpeed = jjDifficulty + 1; //1, 2, 3, 4
if (obj.xPos < targetX) {
obj.direction = 1;
if (obj.xSpeed < maxXSpeed) obj.xSpeed += 0.075;
} else {
obj.direction = -1;
if (obj.xSpeed > -maxXSpeed) obj.xSpeed -= 0.075;
}
if (obj.yPos < targetY) {
if (obj.ySpeed < 0.8) obj.ySpeed += 0.075;
} else {
if (obj.ySpeed > -0.8) obj.ySpeed -= 0.075;
}
obj.xPos += obj.xSpeed;
obj.yPos += obj.ySpeed;
const float absXSpeed = abs(obj.xSpeed);
int targetAngle = int(atan2(obj.ySpeed, absXSpeed > 1.f ? absXSpeed : 1.f) * 162.974662f);
if (obj.xSpeed > 0) targetAngle *= -1;
if (targetAngle > obj.special) obj.special += 2;
else obj.special -= 2;
if (jjDifficulty > 1 && obj.energy <= 30 && BossBalls.length == 1)
BossBalls.insertLast(BossBall());
obj.age += 8;
obj.frameID = (obj.age >> 6) & 3; //ball
for (uint i = 0; i < BossBalls.length; ++i) {
const auto@ ball = BossBalls[i];
const int age = obj.age + (i << 9);
const float dist = jjSin(age) * obj.counter;
ball.scale = 1 + jjCos(age) / 4;
ball.xPos = int(obj.xPos + jjCos(obj.special) * dist);
ball.yPos = int(obj.yPos - jjSin(obj.special) * dist);
}
break; }
case STATE::KILL:
obj.state = STATE::DONE;
obj.playerHandling = HANDLING::PARTICLE;
obj.counterEnd = 0;
case STATE::DONE:
if (++obj.counterEnd < 80) { //shake
obj.xPos += int(jjRandom() & 3) - 1.5;
obj.yPos += int(jjRandom() & 3) - 1.5;
} else if (obj.counterEnd == 80) {
for (uint i = 0; i < BossBalls.length; ++i)
BossBalls[i].method(obj, false);
} else if (obj.counterEnd == 255) {
obj.delete();
jjNxt();
} else {
obj.doesHurt = jjSampleLooped(obj.xPos, obj.yPos, SOUND::COMMON_BENZIN1, obj.doesHurt);
if (jjGameTicks & 3 == 1) {
const float xPos = obj.xPos + int(jjRandom() & 63) - 31;
const float yPos = obj.yPos + int(jjRandom() & 63) - 31;
jjObjects[jjAddObject(OBJECT::EXPLOSION, xPos, yPos)].determineCurAnim(ANIM::AMMO, 3 + (jjRandom() & 2));
jjOBJ@ turtlette = jjObjects[jjAddObject(OBJECT::COKE, xPos, yPos, obj.creatorID, CREATOR::OBJECT)];
if (jjRandom() & 1 == 1) turtlette.direction = -1;
}
}
return;
}
for (uint i = 0; i < BossBalls.length; ++i) {
const auto@ ball = BossBalls[i];
const auto ballYPos = ball.yPos - 26;
if (abs(ball.scale - 1.f) < 0.15f) {
for (int j = 0; j < jjLocalPlayerCount; ++j) {
jjPLAYER@ localPlayer = jjLocalPlayers[j];
if (localPlayer.blink == 0 && BallCollisionFrame.doesCollide(ball.xPos, ballYPos, 1, jjAnimFrames[localPlayer.curFrame], int(localPlayer.xPos), int(localPlayer.yPos), localPlayer.direction))
localPlayer.hurt();
}
for (uint j = jjObjectCount; --j != 0;) {
jjOBJ@ bullet = jjObjects[j];
if (bullet.isActive && bullet.playerHandling == HANDLING::PLAYERBULLET && bullet.state != STATE::EXPLODE && BallCollisionFrame.doesCollide(ball.xPos, ballYPos, 1, jjAnimFrames[bullet.curFrame], int(bullet.xPos), int(bullet.yPos), bullet.direction, true))
bullet.state = STATE::EXPLODE;
}
}
}
if (obj.state == STATE::FREEZE)
return;
for (int i = 0; i < jjLocalPlayerCount; ++i) {
jjPLAYER@ localPlayer = jjLocalPlayers[i];
if (localPlayer.invincibility < -2)
localPlayer.invincibility = -2; //antibuttstomp
if (obj.justHit != 0)
localPlayer.specialMove = 0;
}
if (jjDifficulty > 0 && obj.energy < 75 && jjGameTicks & 31 == 0)
for (int i = -1; i < 2; ++i)
AddEnemyBullet(obj, i, -4, 0.25, false);
}
void onDraw(jjOBJ@ obj) {
if (obj.state == STATE::START) //not yet activated
return;
if (obj.state == STATE::DONE && obj.counterEnd >= 80)
return;
for (uint i = 0; i < BossBalls.length; ++i)
BossBalls[i].method(obj, true);
const SPRITE::Mode mainDrawingMode = (obj.state != STATE::FREEZE) ? (obj.justHit == 0) ? SPRITE::NORMAL : SPRITE::SINGLECOLOR : SPRITE::FROZEN; //ship
jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.special, 1,1, mainDrawingMode,15);
if (jjGameTicks & 3 < 2)
jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame + 1, obj.special, 1,1, mainDrawingMode,15); //fire
jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame + 3, obj.special, obj.direction,1, mainDrawingMode,15); //devan
if (obj.state == STATE::BOUNCE || (obj.state == STATE::EXTRA && obj.counterEnd < 80))
jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame + 2, obj.special, 1,1, (obj.state != STATE::FREEZE) ? (obj.justHit == 0) ? SPRITE::TRANSLUCENT : SPRITE::TRANSLUCENTCOLOR : SPRITE::FROZEN,15); //screen
}
}
bool onCheat(string &in cheat) {
if (cheat == "jjgems") {
for (int i = 0; i < jjLocalPlayerCount; ++i)
jjLocalPlayers[i].gems[GEM::RED] = jjLocalPlayers[i].gems[GEM::RED] + 10;
jjAlert("jjgems", false, STRING::MEDIUM);
return true;
}
return false;
}
Jazz2Online © 1999-INFINITY (Site Credits). We have a Privacy Policy. Jazz Jackrabbit, Jazz Jackrabbit 2, Jazz Jackrabbit Advance and all related trademarks and media are ™ and © Epic Games. Lori Jackrabbit is © Dean Dodrill. J2O development powered by Loops of Fury and Chemical Beats.
Eat your lima beans, Johnny.