Name | Author | Game Mode | Rating | |||||
---|---|---|---|---|---|---|---|---|
Miscellaneous stuff | Violet CLM | Multiple | N/A |
uint i; //Personally I get tired of declaring "uint8 i" or whatever every time I do a loop.
//This leads to sign warnings when compared against signed values, so it's hardly best practice,
//but none of the signed values are actually all that large so in this case I can get away with it.
uint elapsed = 0; //rather than use jjGameTicks, we're going to use a dedicated uint and increment it
//every onMain, to make it easier to tell how long something's been going on.
jjPAL daytime;
array<bool> flags(8, false);
bool reachedCheckpoint = false; //Kept outside of the flags array, so it doesn't get reset every onLevelReload().
array<int> objects(10, 0);
jjOBJ@ o, sinePlat, devan, seaPlat;
int devanID;
int slain = 0;
const int SLAINTARGET = 20;
const float DEVANBACKGROUNDSPEED = 5;
const array<OBJECT::Object> devanGoodies = {OBJECT::CARROT, OBJECT::FISH, OBJECT::REDGEM, OBJECT::SEEKERAMMO3, OBJECT::LETTUCE};
const int fPaletteSwap = 0; //The level is broken into several, mostly independent scenes, e.g. the surge
const int fTrig31 = 1; //or the entire underground boat sequence. To know which scene is active at
const int fSurge = 2; //any given time, we set various bools in array<bool> flags, using these named
const int fSurgeEnded = 3; //constants to make sure we're always setting or testing the right bool.
const int fPaletteRevert = 4; //Since the scenes don't really overlap, it could also be possible to have a
const int fPitfall = 5; //single uint8 (or somesuch) specifying what the current scene is, with bools
const int fDevan = 6; //set aside for the concurrent palette swaps -- either would work, and this
const int fBoat = 7; //method was chosen pretty much arbitrarily.
void resetLayer5() {
jjLayerTileHeight[5] = false;
jjLayerXSpeed[5] = 0.149933;
jjLayerYSpeed[5] = 0.0499878;
jjLayerYAutoSpeed[5] = 0;
}
void resetWater() {
jjWaterLighting = WATERLIGHT::GLOBAL;
jjSetWaterLevel((jjLayerHeight[4]-6)*32, true);
}
void onLevelLoad() {
daytime.load("Carrot1.j2t");
resetWater();
jjSetWaterGradient(31,255,63, 0,127,31); //green water for 16-bit color
jjObjectPresets[OBJECT::FISH].energy = 50;
@seaPlat = jjObjects[0]; //Later we'll be checking to see if these are already pointing to the right objects,
@sinePlat = jjObjects[0]; //so they need to be initialized to avoid null pointer exceptions.
}
void onLevelReload() { //Basically, reset everything JJ2 doesn't reset by itself.
elapsed = 0;
slain = 0;
resetWater();
for (i = 0; i < flags.length; i++) flags[i] = false;
for (i = 0; i < objects.length; i++) objects[i] = 0;
jjTriggers[1] = false; //JJ2 does turn off triggers when you die, but this function does double duty
jjTriggers[31] = false; //in that it's called when you start the level over by emerging from the
jjTriggers[0] = false; //cave, so we have to specify these manually.
jjEnabledASFunctions[0] =
jjEnabledASFunctions[1] =
jjEnabledASFunctions[3] =
jjEnabledASFunctions[4] =
jjEnabledASFunctions[99] = true;
jjEventSet(22, 11, AREA::AREAID); //Restore the Path event so the regenerating butterfly at the beginning
//goes up and left instead of right and up.
jjMusicLoad("carrotus.j2b");
if (reachedCheckpoint) { //Restore the appropriate settings for the underground boat scene.
flags[fBoat] = true;
flags[fPaletteSwap] = true; //don't switch to daytime
jjPalette.gradient(31,255,63, 0,127,31, 176,70); //green water for 8-bit color
jjPalette.apply();
p.limitXScroll(90,30); //there's only one local jjPLAYER if this function gets called at all
p.warpToTile(94,59,true);
} else {
resetLayer5(); //In case the player died while fighting Devan.
}
}
void onPlayer() {
if (p.yPos > jjWaterLevel) {
resetWater();
p.kill();
}
if (p.platform != 0 && jjObjects[p.platform].eventID == OBJECT::GRASSPLATFORM) {
if (jjObjects[p.platform].yOrg > 11*32) { //The boat platform
jjObjects[p.platform].xOrg = jjObjects[p.platform].xOrg + ((p.running) ? 2 : 1) * ((p.direction < 0) ? -1 : 1);
} else { //The back-and-forth platform
jjObjects[p.platform].light = 2; //In onMain(), "light" will be deincremented whenever it's
//greater than 0. By constantly setting it to 2, not 1, we
//ensure that the light doesn't have time to fade away until
//the player is no longer standing on the platform.
}
}
if (flags[fDevan]) {
movePlayerToDevanArena(p);
}
}
void onMain() { //This is by far the bulk of the script file, though some of its contents are separated out into
//a whole bunch of subfunctions in the interest of readability. Basically what's happening
//is every gametick, we figure out what scene the level is at by checking the various bools inside
//the "flags" array, then perform code accordingly.
if (!flags[fPaletteSwap]) { //Switching from night to day at the very beginning of the level.
jjPalette.reset();
jjPalette.copyFrom(
1, //start
254, //length (It's best to leave colors 0 and 255 alone.)
1, //start2
daytime, //source
elapsed / 300.0 //opacity
);
jjPalette.apply();
jjSetFadeColors(); //uses palette color 207 by default
if (elapsed == 300) { //End this scene.
flags[fPaletteSwap] = true;
}
}
if (!flags[fTrig31] && jjTriggers[31]) {
doBridgeMotion();
}
if (flags[fSurge] && !flags[fSurgeEnded]) {
doEnemySurge();
} else if (flags[fPaletteRevert]) { //Pretty much a total reverse of fPaletteSwap, but slightly slower
jjPalette.reset();
jjPalette.copyFrom(1,254,1, daytime, 1 - elapsed / 400.0);
jjPalette.apply();
jjSetFadeColors();
if (elapsed == 400) flags[fPaletteRevert] = false;
}
if (flags[fSurgeEnded] && !flags[fPitfall] && !flags[fDevan] && !flags[fBoat]) {
doSinePlatform();
}
else if (flags[fPitfall]) {
doPitfall();
} else if (flags[fDevan]) {
doDevanBattle();
} else if (flags[fBoat]) {
if (seaPlat.eventID != OBJECT::GRASSPLATFORM //most basic check
|| !seaPlat.isActive //in case it got deleted and left a ghost of itself in memory
|| seaPlat.yOrg < 11*32 //we don't want to get the wrong grass platform
) @seaPlat = jjObjects[jjAddObject(OBJECT::GRASSPLATFORM, 94*32, 61*32)];
seaPlat.yOrg = jjWaterLevel;
}
elapsed++;
}
void doBridgeMotion() { //Relevant bridge properties: var[0] is Length, var[1] is Toughness. Neither correspond exactly
//to the numbers written in JCS -- Length is multiplied by 32 or something, and Toughness is
//calcuated through a combination of the JCS-specified length and toughness values.
for (i = 1; i < jjObjectCount; i++) {
@o = jjObjects[i];
if (o.isActive && o.eventID == OBJECT::BRIDGE) { //We're going to edit both bridges with this loop.
if (o.yOrg < 10*32) { //The top one.
if (o.var[0] < 8*32) {
o.var[0] = o.var[0] + 2;
o.yPos = o.yPos - 1;
} else {
o.var[1] = 1; //1 is tough as it can possibly get.
flags[fTrig31] = true; //End the scene. It takes longer for the top
//bridge to extend than for the bottom to recede,
//so it gets the scene-ending code.
}
} else { //The bottom one.
if (o.var[0] > 0) o.var[0] = o.var[0] - 1; //Don't make the length shorter than 0.
}
}
}
}
void doEnemySurge() {
if (elapsed < 240) { //The surge scene is broken into two parts: the initial flashing alarm,
//and then the actual falling lizards. The alarm lasts 240 ticks (just over
//three seconds).
jjPalette = daytime;
jjPalette.fill(255,0,0, jjSin((elapsed)*16) * .333 + .5);
jjSetFadeColors();
jjPalette.apply(); //Always remember to call apply().
//if (elapsed & 15 == 0) jjSample(p.xPos, p.yPos, SOUND::ROBOT_POLE, 128, 0);
}
else if (slain < SLAINTARGET) {
if (elapsed == 240) daytime.apply(); //It would be more elegant to ensure that the
//tinting ended with 0 opacity at elapsed==239,
//but elegance is hard
/****************************************************************
Okay, so let's explain how this whole thing works, since it's a bit complex.
At the center of the surge is "objects", a 10-long array of integers which are
potential object IDs. Every tick, we loop through "objects" in search of any
integer that _used_ to point to a lizard (i.e. isn't 0) but no longer does
(either the object at jjObjects[objects[i]] no longer exists, or its slot has
been taken by something else, e.g. a bullet). The existence of such a slot
means that a lizard has been killed. Thus "slain" should be incremented by 1,
and the integer is set to 0 so that it doesn't get read more than once.
Meanwhile, once per second we loop through "objects" AGAIN in search of any
integer that equals 0, either because it's never pointed to a lizard or because
it used to but then that lizard was killed and so the integer was reset to 0 above.
if such an integer is found, we call jjAddObject to drop a lizard into the scene,
and the return value is inserted into "objects" appropriately. All told, there can
never be more than 10 (the length of "objects") lizards alive at a time, and whenever
one stops existing, we'll notice, increment "slain", and remove the pointer to it.
The 1.23/TSF Lizard/XMas Lizard alternation is purely for the sake of showing off.
Um, showing off the use of the jjIsTSF global property, I mean.
Also, we keep setting the energy of the current activated boss Fish
to 50 - slain * (50/SLAINTARGET), which is to say 50 - (slain * 2.5)
since SLAINTARGET is 20. Since the native energy of a Fish is now 50,
and the meter displays energy as percentage of native initial energy,
this gets translated pretty straightforwardly to the boss health meter.
****************************************************************/
jjObjects[jjLocalPlayers[0].boss].energy = 50 - slain * 50 / SLAINTARGET; //Set the boss health meter
for (i = 0; i < objects.length; i++) {
if (objects[i] != 0 && (!jjObjects[objects[i]].isActive || !(jjObjects[objects[i]].eventID == OBJECT::FLOATLIZARD || jjObjects[objects[i]].eventID == OBJECT::XMASFLOATLIZARD))) {
objects[i] = 0;
slain++; //A lizard is dead!
}
}
if (elapsed % 70 == 0) { //Once per second
for (i = 0; i < objects.length; i++) {
if (objects[i] == 0) { //Has to be an empty slot. Trying to replace a full slot would
//result in potentially more than 10 lizards, and the one/s
//pointed to by the overwritten slot/s would not increment the
//slain counter when killed.
uint16 randomTarget = (45 + (jjRandom()&15)) * 32; //&15 is more random than %20,
//even if it's a smaller range
objects[i] = jjAddObject((jjIsTSF) ? OBJECT::XMASFLOATLIZARD : OBJECT::FLOATLIZARD, randomTarget, 0);
jjObjects[objects[i]].state = STATE::FALL;
jjObjects[objects[i]].var[5] = 0; //var[5] is a boolean for leaving a copter behind.
//You figure these things out through careful
//experimentation or by asking the Plus dev team.
break;
}
}
}
} else { //slain >= SLAINTARGET: surge is over
flags[fSurgeEnded] = true;
jjMusicLoad("carrotus.j2b");
jjObjects[jjLocalPlayers[0].boss].state = STATE::KILL; //garbage collection of sorts
jjLocalPlayers[0].activateBoss(false);
jjLocalPlayers[0].boss = 0;
jjLocalPlayers[0].showText("@@@@@SURGE COMPLETE!");
for (i = 0; i < jjLocalPlayerCount; i++) {
jjLocalPlayers[i].limitXScroll(43, 0);
}
flags[fPaletteRevert] = true; //Start moving the palette back towards nighttime.
elapsed = 0;
}
}
void doSinePlatform() {
/****************************************************************
It turns out that swinging platforms are among the trickiest object types to manipulate,
which makes them either a good choice or a bad choice for this example level, depending
on how you'd like to look at it. The main problem with them is that unlike most objects,
setting their xPos/yPos properties does no good. They rotate around their point of origin,
and so you need to set xOrg and yOrg.
Unfortunately, changing an object's origins introduces a whole nest of problems. If an object
is created from the event map, i.e. it is simply placed in JCS as a non-regenerating event, it
will have creatorType CREATOR::LEVEL. What this means is that when it's created, a bit is set
at that tile (that xOrg/yOrg tile) saying "my object exists, don't try to create it again."
When the object passes out of memory in single player -- when its state is STATE::DEACTIVATE --
it will turn off the bit, and in the process, set the event at that tile to itself just in case.
However, if you've CHANGED xOrg and yOrg in the meantime, state==STATE::DEACTIVATE will overwrite
the event at whatever tile the object has moved to by the time it gets deactivated. That can
be avoided by setting its creator to 0, but then the original "my object exists" bit will never
be set to 0, so once the object first passes out of memory, it will never appear again.
This is why changing xOrg and yOrg is usually a bad idea. But, swinging platforms require it.
Instead we take a second option: whenever jjOBJ@ sinePlat doesn't already refer to an existing
grass platform, create a NEW grass platform for it to refer to! The "MCE Event" event at tile
79,8 (80,9 in JCS) is carefully set up to have the exact bitfield we want to use for the
platform's parameters, and once it's created there, we can move it around freely. It'll pass out
of memory and get destroyed A LOT, but every time it gets destroyed, doSinePlatform() will notice
that sinePlat no longer refers to an existing grass platform, so it'll get created again! Also,
because its creator property equals 0, destroying it will have no ill effects on the event map.
****************************************************************/
if (sinePlat.eventID != OBJECT::GRASSPLATFORM //most basic check
|| !sinePlat.isActive //in case it got deleted and left a ghost of itself in memory
|| sinePlat.yOrg >= 11*32 //we don't want to get the wrong grass platform
) {
@sinePlat = jjObjects[jjAddObject(OBJECT::GRASSPLATFORM, 79*32, 8*32)];
sinePlat.lightType = LIGHT::LASER; //But sinePlat.light == 0, so this won't be visible
//until it gets stood on. When light == 0, lightType
//doesn't matter at all.
}
sinePlat.xOrg = (79*32) + jjSin(jjGameTicks*4)*(7*32);
if (sinePlat.light > 0) sinePlat.light--; //Standing on the platform changes its light to 2.
}
void doPitfall() { //This is all quite straightforward use of the jjOBJ class. The OBJECT::EXPLOSION
//object type may be less familiar, but it's very easy to use: create it and give
//it an animation, and it'll play that animation once then destroy itself. Convenient!
if (elapsed < 430) {
if (elapsed & 1 == 0) {
jjOBJ@ meteor = jjObjects[jjAddObject(OBJECT::FIRESHIELDBULLET, (102 + (jjRandom()&31)) * 32, 2)];
//Pretty much any bullet type would work as well, but this one is nice for the sparks.
meteor.xSpeed = -6;
meteor.ySpeed = 7;
meteor.lightType = LIGHT::POINT2;
meteor.determineCurAnim(ANIM::AMMO, 15);
}
}
if (elapsed > 160 && elapsed % 3 == 0) {
jjOBJ@ explosion = jjObjects[jjAddObject(OBJECT::EXPLOSION, (100 + (jjRandom()%20)) * 32, 7.5*32+(jjRandom()&63))];
explosion.determineCurAnim(ANIM::AMMO, 2);
explosion.lightType = LIGHT::BRIGHT;
explosion.light = 9;
jjSample(explosion.xPos, explosion.yPos, SOUND::AMMO_BOEM1, 128, 0);
}
if (elapsed >= 470) jjTriggers[1] = true; //Make the floor go away.
}
void doDevanBattle() {
//First, we basically reuse the code from the Surge to create and keep track of a number of objects,
//in this case gleaned from the OBJECT::Object array "devanGoodies". You should be able to figure this
//stuff out more or less on your own at this point.
if (elapsed & 63 == 0) {
for (i = 0; i < objects.length; i++) {
if (objects[i] == 0) {
objects[i] = jjAddObject(devanGoodies[jjRandom()%devanGoodies.length], 100 * 32 + jjRandom()%(32*19), 44*32);
if (jjObjects[objects[i]].eventID == OBJECT::FISH) {
jjObjects[objects[i]].determineCurAnim(ANIM::ROBOT,0); //The robot boss's spike ball.
}
jjObjects[objects[i]].lightType = LIGHT::BRIGHT;
jjObjects[objects[i]].light = 10;
break;
}
}
}
for (i = 0; i < objects.length; i++) if (objects[i] != 0) {
if (jjObjects[objects[i]].isActive) { //Occasionally an object ID will get reused for a new object
//that was previously used for an old one and before the slot
//referencing that old one had a chance to be removed. When
//this happens, the object will move twice as fast upwards.
//This could be addressed somehow, but it's not really a big deal.
jjObjects[objects[i]].ySpeed = 0; //Otherwise it would be possible to shoot down the goodies.
jjObjects[objects[i]].yPos = jjObjects[objects[i]].yPos - DEVANBACKGROUNDSPEED;
} else {
objects[i] = 0;
}
}
/****************************************************************
The punchline to this whole scene is that Plus does not add a "Falling Devan" boss object
or anything of the sort. Instead, what you're fighting is a Fish enemy, and all of its
behavior is defined inside this .j2as file! Devan is created in onFunction3 below, and from
then on it's this switch statement that handles everything until you finally beat him.
This is actually pretty much the same way that actual object code works inside of JJ2! One
big switch statement based on the object's current state. We can't actually change the
"state" property itself, though, since this is still a Fish on the inside and it won't
display itself correctly if you change "state" to anything it doesn't expect. Instead,
we're going to use "oldState" -- really this is a property dedicated for remembering what
an object was doing before it got frozen, but there's no ice ammo in this level, so it's
safe to commandeer it. Other properties, e.g. "special" would presumably also work, but
"oldState" has "state" in its name, so it's just slightly easier to read the code this way.
We use Fish because for the most part, it's a very simple object type to use for custom
enemies and bosses: unless it's below water, it never does anything at all. The only real
downside is that it's hard to animate it. Many objects sensibly constantly set their frameID
according to jjGameTicks / animSpeed % (number of frames in the animation) or something
like that; Fish constantly increments its counter property, and only increments frameID when
counter reaches 16. Thus increasing its animation speed can be a bit of a pain, but
if (devan.counter & 15 == 1) devan.counter = 10;
does the trick.
Incidentally, expect future revisions of Plus to make this more straightforward, i.e. no
more relying on Fish or other objects unless you really want to. This is what you have to
work with right now, but it'll get better.
****************************************************************/
if (devan.energy < 5) { //mortally wounded for present purposes
devan.energy = 75; //so the above condition won't be true anymore
devan.oldState = STATE::KILL;
devan.noHit = 1; //can't be shot
devan.determineCurAnim(ANIM::DEVAN, 4); //rotating
devan.ySpeed = -5;
devan.xSpeed = -3 * devan.direction;
jjLocalPlayers[0].activateBoss(false);
jjLocalPlayers[0].boss = 0;
}
switch(devan.oldState) {
case STATE::FLOAT:
devan.direction = (devan.xPos > jjLocalPlayers[0].xPos) ? -1 : 1;
//Always look towards player 1.
devan.yPos = devan.yOrg + jjSin(jjGameTicks*4)*10;
//Float up and down a little, try to make it look like it's falling.
if (devan.counterEnd++ == 35) { //If Devan is idling, which is the whole point of STATE::FLOAT,
devan.counterEnd = 0; //every half second he randomly does something else! Sometimes
//that something else is nothing; sometimes it's shooting a bullet;
//other times he changes his state (oldState) to something else
//entirely and doesn't return to STATE::FLOAT for a while.
jjOBJ@ bullet;
switch(jjRandom()&31) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: //do nothing
break;
case 10: case 11: case 12: //fall up
devan.oldState = STATE::BOUNCE;
devan.var[10] = 1; //Call randomizeDevanTarget() once done bouncing.
devan.yOrg = 25*32; //Target yPos.
break;
case 13: case 14: case 15: //fall down
devan.oldState = STATE::FALL;
devan.var[10] = 1; //Call randomizeDevanTarget() once done falling.
devan.yOrg = 43*32; //Target yPos.
break;
case 16: case 17: case 18: case 19: //teleport
devan.oldState = STATE::FADEOUT;
jjSample(devan.xPos, devan.yPos, SOUND::COMMON_TELPORT1, 0, 0);
devan.determineCurAnim(ANIM::DEVAN, 2);
devan.noHit = 1; //can't be shot
devan.frameID = 0;
devan.counter = 0;
break;
default: //shoot
@bullet = jjObjects[jjAddObject(OBJECT::BLASTERBULLETPU, devan.xPos, devan.yPos, devanID, CREATOR::OBJECT)];
bullet.determineCurAnim(ANIM::DEVAN, 0);
bullet.objType = 2; //double damage
bullet.noHit = 1; //doesn't block bullets
bullet.xSpeed = devan.direction * 4.5;
bullet.counterEnd += 10; //lasts longer
break;
}
}
break;
case STATE::FADEOUT: //warp away
if (devan.counter & 15 == 1) devan.counter = 10; //animate faster
if (devan.frameID == 6) {
devan.oldState = STATE::FADEIN;
jjSample(devan.xPos, devan.yPos, SOUND::COMMON_TELPORT2, 0, 0);
devan.determineCurAnim(ANIM::DEVAN, 5);
devan.frameID = 0;
randomizeDevanTarget();
devan.yPos = devan.yOrg;
}
break;
case STATE::FADEIN: //finish warping
if (devan.counter & 15 == 1) devan.counter = 10; //animate faster
if (devan.frameID == 6) {
devan.oldState = STATE::FLOAT;
devan.determineCurAnim(ANIM::DEVAN, 3);
devan.noHit = 0;
devan.frameID = 0;
}
break;
case STATE::FALL: //move down
devan.direction = (devan.xPos > jjLocalPlayers[0].xPos) ? -1 : 1; //Always look towards player 1.
if (devan.yPos < devan.yOrg) {
devan.yPos = devan.yPos + devan.ySpeed;
} else if (devan.var[10] == 1) { //Just fell offscreen; now bounce back somewhere else!
devan.oldState = STATE::BOUNCE;
devan.var[10] = 0;
randomizeDevanTarget();
} else {
devan.oldState = STATE::FLOAT;
}
break;
case STATE::BOUNCE: //move up
devan.direction = (devan.xPos > jjLocalPlayers[0].xPos) ? -1 : 1; //Always look towards player 1.
if (devan.yPos > devan.yOrg) {
devan.yPos = devan.yPos - devan.ySpeed;
} else if (devan.var[10] == 1) { //Just bounced offscreen; now fall back somewhere else!
devan.oldState = STATE::FALL;
devan.var[10] = 0;
randomizeDevanTarget();
} else {
devan.oldState = STATE::FLOAT;
}
break;
case STATE::KILL: //defeated
if (devan.yPos < 43*32) {
devan.xPos = devan.xPos + devan.xSpeed;
devan.yPos = devan.yPos + devan.ySpeed;
devan.ySpeed = devan.ySpeed + .1;
} else { //End the scene and set up the next one.
devan.state = STATE::KILL; devan.counter = 0; devan.energy = 0;
jjTriggers[0] = true;
flags[fDevan] = false;
if (jjLocalPlayerCount > 1) //No boat scene for cooperative play, just wouldn't work
jjNxt();
else {
flags[fBoat] = true;
reachedCheckpoint = true;
jjTriggers[31] = false; //so the bridges don't trigger themselves again
//when revisited before they're even visible
jjPalette.gradient(31,255,63, 0,127,31, 176,70); //green water for 8-bit color
jjPalette.apply();
jjMusicLoad("carrotus.j2b");
jjLayerYAutoSpeed[5] = 0;
jjLayerTileHeight[5] = false;
for (i = 0; i < objects.length; i++) jjObjects[objects[i]].light = 0;
p.lighting = 60; //we know there's only one local player,
p.fly = FLIGHT::NONE; //so it's safe here to use "p" in onMain()
p.cameraUnfreeze(); //
p.limitXScroll(90,30); //
p.xOrg = 100*32; //Like a checkpoint, but cleaner
p.yOrg = 59*32; //
}
}
break;
}
}
void randomizeDevanTarget() { //Used whenever Devan warps away or has fallen/bounced offscreen and is ready to come back
devan.xPos = 100 * 32 + jjRandom()%(32*19) + 16;
devan.yOrg = 27 * 32 + jjRandom()%(32*14) + 16;
}
void movePlayerToDevanArena(jjPLAYER@ play) {
if (play.xPos < 99*32) play.xPos = 110*32; //Retrieve tardy/dead cooperative players.
if (play.yPos < 25*32) {
play.warpToTile(play.xPos/32,35, true);
play.fly = FLIGHT::FLYCARROT;
if (play.localPlayerID == 0 && play.boss == 0) {
play.activateBoss();
play.boss = devanID;
}
switch (jjLocalPlayerCount) { //Set up the cameras so that the entire 640x480 space is visible
//at once, shared across the separate splitscreen cameras.
//Assuming the level's being played in 640x480, of course --
//otherwise this won't look right at all, but there's nothing
//to be done about that.
case 1:
play.cameraFreeze(100*32, 27*32, false, true);
break;
case 2:
play.cameraFreeze(100*32, 27*32 + (i&1)*240, false, true);
break;
default:
play.cameraFreeze(100*32 + (i&1)*320, 27*32 + (i&2)*120, false, true);
break;
}
}
}
void onFunction0() { //switch initial butterfly's path to go right instead of up
jjEventSet(22,11,0);
}
void onFunction1() { //start the surge
flags[fSurge] = true;
elapsed = 0;
jjMusicLoad("fastrack.j2b"); //Mostly just for the sake of changing it, period.
jjLocalPlayers[0].activateBoss();
jjLocalPlayers[0].boss = jjAddObject(OBJECT::FISH, 37*32, 21*32); //Carrier of the "health" progress.
jjObjects[jjLocalPlayers[0].boss].determineCurAnim(ANIM::BOSS, 0);
//There are no actual boss events in the level, so JJ2 doesn't know it should
//load the sprites for the boss health meter. By assigning them to this Fish,
//which is safely off-screen, we ensure the game doesn't crash immediately.
jjLocalPlayers[0].showText("@@@@@ ENEMY SURGE COMING@@@@@KILL 20 LIZARDS TO CONTINUE!");
//Ideally "20" would be calculated from the value of SLAINTARGET, but
//we don't have number-to-string conversion implemented yet.
for (i = 0; i < jjLocalPlayerCount; i++) {
jjLocalPlayers[i].limitXScroll(43, 20);
}
}
void onFunction2() { //start the pitfall scene
p.limitXScroll(100,20);
if(!flags[fPitfall]) {
flags[fPitfall] = true;
elapsed = 0;
}
}
void onFunction3() { //begin Devan scene
flags[fPitfall] = false; //stop creating explosions
flags[fDevan] = true;
for (i = 0; i < objects.length; i++) objects[i] = 0;
jjLayerTileHeight[5] = true; //Repurpose layer 5 to be an automatically scrolling
jjLayerXSpeed[5] = 1; //layer (which doesn't disappear in low detail mode).
jjLayerYSpeed[5] = 1; //
jjLayerYAutoSpeed[5] = DEVANBACKGROUNDSPEED; //
devanID = jjAddObject(OBJECT::FISH, 103*32, 30*32);
@devan = jjObjects[devanID];
for (i = 0; i < jjLocalPlayerCount; i++) movePlayerToDevanArena(jjLocalPlayers[i]);
jjMusicLoad("boss2.j2b") || jjMusicLoad("boss.j2b"); //A very handy way of changing to the first specified
//music file that JJ2 can find, since the second
//function call won't be evaluated if the first
//returns true.
devan.ySpeed = 3.5; //Will be referenced later for how fast he moves up and down. Could have been an
//an external constant or something, but this is more semantic.
devan.determineCurAnim(ANIM::DEVAN, 3);
devan.oldState = STATE::FLOAT; //As described above, we're using "oldState" instead of "state"
//because Fish is picky about such things.
}
void onFunction4() { //start the boat going
jjSetWaterLevel(26*32,false);
p.limitXScroll(14,20);
p.noFire = true;
p.showText(0,0);
}
void onFunction5() { //reset everything
reachedCheckpoint = false;
p.noFire = false;
p.limitXScroll(2, 118);
p.resetLight();
p.xOrg = 0;
p.yOrg = 0;
onLevelReload();
}
void onFunction99() { //Touched active carrot sign
switch(p.charCurr) {
case CHAR::JAZZ: jjSample(p.xPos, p.yPos, SOUND::JAZZSOUNDS_JUMMY, 0, 0); break;
case CHAR::SPAZ: jjSample(p.xPos, p.yPos, SOUND::SPAZSOUNDS_HAPPY, 0, 0); break;
default: jjSample(p.xPos, p.yPos, SOUND::COMMON_EAT4, 0, 0); break;
}
p.health = 5;
p.invincibility = 300;
for (i = 1; i < jjObjectCount; i++) {
@o = jjObjects[i];
if (o.isActive && o.eventID == OBJECT::STEADYLIGHT) o.lightType = LIGHT::NONE;
}
}
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.