Name | Author | Game Mode | Rating | |||||
---|---|---|---|---|---|---|---|---|
![]() |
Diamondus Ultimate![]() |
DoubleGJ | Tileset conversion | 10 | ![]() |
|||
![]() |
Jungle Ultimate![]() |
DoubleGJ | Tileset conversion | 10 | ![]() |
/* ===Demon Dash dialogue script v1.4===
Written by Violet CLM, minor adjustments by Gnegon Galek
Font in animation file is Kangaroo Court Rotalic
Version history:
v1.1: added scripted events support, use the following format in the same manner as top/bottom line:
finish: function() { jjPrint("hello world"); }
v1.2: sound effects added
v1.3: Mastah Yoman added to character pool, needs color palette entries 248-254 allocated for him, which are unused in every Nick Stadler tileset
v1.4: fixed a null pointer exception occuring when CurrentPopup is initiated by anything other than onFunction# (thx Speaktrap!)
v1.5: Prevented accidental screen skipping, added provisions for callback functions to be called when a whole dialogue is skipped, added multiple choice screens
v1.6: Fixed a bug that caused players to lose invincibility after a dialogue scene
---PUT IN LEVEL SCRIPT, MERGE WITH SAME FUNCTIONS IF NEEDED:---
void onLevelLoad() {
initiateDialogue();
// EXTRA LINE ONLY IF LEVEL INCLUDES MASTAH YOMAN:
YomanPalette();
}
bool onDrawScore(jjPLAYER@ play, jjCANVAS@ canvas) {
if (CurrentPopup !is null) {
CurrentPopup.Draw(canvas);
return !CurrentPopup.DrawHUD();
}
return false;
}
bool onDrawAmmo(jjPLAYER@ play, jjCANVAS@ canvas) { return CurrentPopup !is null && !CurrentPopup.DrawHUD(); }
bool onDrawHealth(jjPLAYER@ play, jjCANVAS@ canvas) { return CurrentPopup !is null && !CurrentPopup.DrawHUD(); }
bool onDrawLives(jjPLAYER@ play, jjCANVAS@ canvas) { return CurrentPopup !is null && !CurrentPopup.DrawHUD(); }
bool onDrawPlayerTimer(jjPLAYER@ play, jjCANVAS@ canvas) { return CurrentPopup !is null && !CurrentPopup.DrawHUD(); }
void onPlayer(jjPLAYER@ play) {
dialogueInterface(play); // or whatever alias as input above
}
---APPLY TO DESIRED DIALOGUE EVENT:---
@CurrentPopup = Conversation(array<Screen@> = {
Screen(top: Line("this is an example of the ORDERLOG system! press FIRE or SELECT to continue to the next screen.", left: Jazz, direction: -1)),
Screen(
top: Line("you can display multiple characters onscreen at once. any one of them can be given a speech box.", right: Lori, direction: 1, color: 88),
bottom: Line(left: Addie)
),
Screen(
top: Line("character corners are completely arbitrary, so are text box colors.", left: Eva, right: Jazz, direction: -1, color: 64),
bottom: Line(left: Devan, right: Eva)
),
Screen(bottom: Line("text can also appear at the bottom of the screen. text box tails can point to empty corners for characters who are just out of sight.", direction: 1)),
Screen(
top: Line("if textboxes appear on both the top and bottom of the screen, they fill up with letters simultaneously.", color: 24),
bottom: Line("wow TWO spazes! just like in multiplayer!", left: Spaz, right: Spaz, direction: -1, color: 48)
),
Screen(
top: Line("we sure are glad you came and played through this example level!", left: Jazz, right: Spaz, direction: -1),
bottom: Line("I'M not.", left: Eva, right: Devan, direction: 1, color: 80)
)
});
*/
#pragma require "DD-Anims.j2a"
const jjANIMATION@ OrderFontAnim;
ANIM::Set OrderPortraitSet;
uint OrderPointingFingerCurFrame;
enum Portrait { Devan, Eva, Jazz, Spaz, Addie, Lori, Bubba, Queen, Yoman, Jill, Chief, _LAST };
Popup@ CurrentPopup = null;
const uint8 FillColor = 15;
const uint8 StrokeColor = 71;
const array<uint8> MultiplyColors = {74, 42, 82, 90, 34, 50, 66};
uint MultiplyColorsID = 0;
const int LineSize = 17;
const int StrokeSize = 2;
const int FillSize = LineSize - StrokeSize * 2;
jjTEXTAPPEARANCE OrderTextAppearance(STRING::NORMAL);
array<int> storeInvincibility(4);
void initiateDialogue() {
OrderTextAppearance.pipe = STRING::DISPLAYSIGN;
OrderTextAppearance.spacing -= 3;
uint8 i;
for (i = 0; i < 256; ++i)
if (jjAnimSets[ANIM::CUSTOM[i]] == 0) {
const int fontAnimID = jjAnimSets[ANIM::CUSTOM[i++]].load(0, "DD-Anims.j2a").firstAnim;
@OrderFontAnim = jjAnimations[fontAnimID];
OrderPointingFingerCurFrame = jjAnimations[fontAnimID + 1].firstFrame;
break;
}
for (; i < 256; ++i)
if (jjAnimSets[ANIM::CUSTOM[i]] == 0) {
jjAnimSets[OrderPortraitSet = ANIM::CUSTOM[i]].load(1, "DD-Anims.j2a");
break;
}
}
void dialogueInterface(jjPLAYER@ play) {
const bool inCutscene = CurrentPopup !is null;
jjCharacters[play.charCurr].canRun = !inCutscene; //block revving
if (inCutscene) {
play.timerPause();
play.invincibility = -15;
if (!CurrentPopup.Do())
@CurrentPopup = null;
PressingKeyToMakeCutsceneGoAway = true;
play.keyLeft = play.keyRight = play.keyDown = play.keyUp = play.keyRun = play.keyFire = play.keyJump = play.keySelect = false;
play.idle = 0;
} else {
if (play.keyFire || play.keySelect) {
if (PressingKeyToMakeCutsceneGoAway)
play.keySelect = play.keyFire = false;
} else
PressingKeyToMakeCutsceneGoAway = false;
play.timerResume();
}
}
void YomanPalette() {
jjPAL pal = jjPalette;
pal.color[248] = jjPALCOLOR(113, 255, 255);
pal.color[249] = jjPALCOLOR(83, 237, 224);
pal.color[250] = jjPALCOLOR(78, 219, 201);
pal.color[251] = jjPALCOLOR(56, 176, 181);
pal.color[252] = jjPALCOLOR(30, 133, 144);
pal.color[253] = jjPALCOLOR(21, 99, 111);
pal.color[254] = jjPALCOLOR(11, 67, 80);
pal.color[255] = jjPALCOLOR(0, 11, 11);
//pal.color[71] = jjPALCOLOR(11, 7, 7);
pal.apply();
}
abstract class Popup {
Popup(){}
bool Do() {
return true;
}
void Draw(jjCANVAS@ canvas) const { }
bool DrawHUD() const { return false; }
}
class Line {
string Text;
Portrait PLeft, PRight;
uint8 Color;
int TailDirection;
array<string> WrappedLines;
array<int> LinesX;
int Left, Top, Width, Height;
Line(const string &in text = "", Portrait left = Portrait::_LAST, Portrait right = Portrait::_LAST, uint8 color = 16, int direction = 0) { Text = text; PLeft = left; PRight = right; Color = color; TailDirection = direction; }
void Wrap(Portrait pLeft, Portrait pRight, bool isTop) {
const bool tooSmallForPortraits = jjSubscreenWidth < 640 || jjSubscreenHeight < 400;
Left = (tooSmallForPortraits || (pLeft == Portrait::_LAST && TailDirection >= 0)) ? 8 : 173;
Width = jjSubscreenWidth - ((tooSmallForPortraits || (pRight == Portrait::_LAST && TailDirection <= 0)) ? 8 : 173) - Left;
const int maxWidth = Width - 16;
WrappedLines.resize(0);
LinesX.resize(0);
if (Text.length != 0) {
string line = "";
const array<string> words = Text.split(" ");
for (uint wordID = 0; wordID < words.length; ++wordID) {
if (jjGetStringWidth(line + words[wordID], OrderFontAnim, OrderTextAppearance) >= maxWidth) {
WrappedLines.insertLast(line);
LinesX.insertLast(Left + (Width - jjGetStringWidth(line, OrderFontAnim, OrderTextAppearance)) / 2);
line = words[wordID] + " ";
} else
line += words[wordID] + " ";
}
WrappedLines.insertLast(line);
LinesX.insertLast(Left + (Width - jjGetStringWidth(line, OrderFontAnim, OrderTextAppearance)) / 2);
}
Height = WrappedLines.length * 20 + 16;
Top = (isTop ? (tooSmallForPortraits ? 30 : 60) : jjSubscreenHeight - (tooSmallForPortraits ? 30 : 60)) - Height / 2;
if (isTop) {
if (Top < 7)
Top = 7;
} else {
if (Top + Height > jjSubscreenHeight - 7)
Top = jjSubscreenHeight - 7 - Height;
}
}
}
funcdef void FINISHORSKIP(bool);
enum ChoiceStyle {
NormalChoice, DefaultIfSkippedChoice, HiddenAndDefaultChoice
}
class Choice {
string Text;
uint8 Color;
jjVOIDFUNC@ Effect;
ChoiceStyle Style;
Choice(string text = "", jjVOIDFUNC@ effect = null, ChoiceStyle style = NormalChoice, uint8 color = 0) {
Text = text;
Color = color;
Style = style;
@Effect = @effect;
if (Effect is null)
@Effect = function(){};
}
}
class Screen {
array<Portrait> portraits(4, Portrait::_LAST);
array<Line@> boxes(2);
array<Choice@>@ choiceList;
uint maximumChoiceLength;
int defaultChoice = -1;
uint choiceCount = 0;
jjVOIDFUNC@ finishCallback;
FINISHORSKIP@ finishOrSkipCallback;
void AssignTopAndBottom(Line@ top, Line@ bottom, bool showAlert) {
@boxes[0] = top;
@boxes[1] = bottom;
if (top is null && bottom is null) {
if (showAlert)
jjAlert("Please define at least one line!");
} else {
if (top !is null) {
portraits[0] = top.PLeft;
portraits[2] = top.PRight;
}
if (bottom !is null) {
portraits[1] = bottom.PLeft;
portraits[3] = bottom.PRight;
}
}
}
void SetupChoiceList(array<Choice@>@ choices) {
if (choices is null)
@choiceList = array<Choice@>(0);
else
@choiceList = choices;
for (uint i = 0; i < choiceList.length; ++i) {
const Choice@ choice = choiceList[i];
if (choice is null) {
choiceList.removeAt(i--);
continue;
}
if (choice.Style != NormalChoice) {
if (defaultChoice == -1)
defaultChoice = i;
else
jjAlert("Only one choice can be the default one!");
}
if (choice.Style != HiddenAndDefaultChoice)
choiceCount += 1;
}
}
Screen(Line@ top = null, Line@ bottom = null, jjVOIDFUNC@ finish = null, array<Choice@>@ choices = null) {
@finishCallback = finish;
SetupChoiceList(choices);
AssignTopAndBottom(top, bottom, choices is null || choices.length == 0);
}
Screen(FINISHORSKIP@ finish, Line@ top = null, Line@ bottom = null, array<Choice@>@ choices = null) {
@finishOrSkipCallback = finish;
@choiceList = choices;
SetupChoiceList(choices);
AssignTopAndBottom(top, bottom, choices is null || choices.length == 0);
}
}
class Conversation : Popup {
bool Started = false;
bool LastSelect = true, LastUp = true, LastDown = true;
array<array<int>> PortraitX(4, array<int>(Portrait::_LAST, 180));
array<Portrait> PortraitPerCorner(4, Portrait::_LAST);
const array<Screen@>@ Screens;
uint ScreenID = 0;
uint CharactersToDraw;
uint MaxCharactersToDraw;
uint TicksOnThisScreen;
int LastSubscreenSize;
int SelectedChoice = 0;
Conversation(const array<Screen@>@ screens) {
for (int i = 0; i < 4; i++) storeInvincibility[i] = jjLocalPlayers[i].invincibility;
@Screens = screens;
}
void StartScreen() {
Started = true;
Screen@ screen = Screens[ScreenID];
if (screen is null)
return;
++MultiplyColorsID;
for (int cornerID = 0; cornerID < 4; ++cornerID)
PortraitPerCorner[cornerID] = screen.portraits[cornerID];
CharactersToDraw = 0;
TicksOnThisScreen = 0;
if (screen.boxes[0] is null)
MaxCharactersToDraw = screen.boxes[1].Text.length;
else if (screen.boxes[1] is null)
MaxCharactersToDraw = screen.boxes[0].Text.length;
else {
MaxCharactersToDraw = screen.boxes[0].Text.length;
if (screen.boxes[1].Text.length > MaxCharactersToDraw)
MaxCharactersToDraw = screen.boxes[1].Text.length;
}
screen.maximumChoiceLength = 0;
for (uint i = 0; i < screen.choiceList.length; ++i) {
const Choice@ choice = screen.choiceList[i];
const uint thisChoiceLength = jjGetStringWidth(choice.Text, OrderFontAnim, OrderTextAppearance);
if (thisChoiceLength > screen.maximumChoiceLength)
screen.maximumChoiceLength = thisChoiceLength;
}
screen.maximumChoiceLength += 40; //padding
LastUp = LastDown = true;
SelectedChoice = 0;
WrapBoxes();
}
void WrapBoxes() {
LastSubscreenSize = jjSubscreenWidth | (jjSubscreenHeight << 16);
if (Screens[ScreenID].boxes[0] !is null)
Screens[ScreenID].boxes[0].Wrap(PortraitPerCorner[0], PortraitPerCorner[2], true);
if (Screens[ScreenID].boxes[1] !is null)
Screens[ScreenID].boxes[1].Wrap(PortraitPerCorner[1], PortraitPerCorner[3], false);
}
bool Do() {
if (!Started) {
ScreenID = 0;
StartScreen();
}
if (Screens[ScreenID] is null)
return Finish();
if (LastSubscreenSize != jjSubscreenWidth | (jjSubscreenHeight << 16))
WrapBoxes();
for (int cornerID = 0; cornerID < 4; ++cornerID) {
array<int>@ corner = PortraitX[cornerID];
for (int portraitID = 0; portraitID < Portrait::_LAST; ++portraitID) {
if (PortraitPerCorner[cornerID] == portraitID) {
if (corner[portraitID] > 0)
corner[portraitID] -= 6;
} else {
if (corner[portraitID] < 180)
corner[portraitID] += 6;
}
}
if (CharactersToDraw < MaxCharactersToDraw)
jjSamplePriority(SOUND::MENUSOUNDS_TYPE);
}
Screen@ screen;
if (jjKey[8]) { //backspace
const auto initialScreenID = ScreenID;
do {
@screen = Screens[ScreenID];
if (screen is null)
continue;
if (screen.choiceList.length != 0) {
if (screen.defaultChoice < 0) { //one or more choices, but none of them are the default
if (ScreenID != initialScreenID) //at least one screen was skipped
StartScreen();
break;
}
screen.choiceList[screen.defaultChoice].Effect(); //could be either DefaultIfSkipped or HiddenAndDefault, doesn't matter
}
if (screen.finishOrSkipCallback !is null)
screen.finishOrSkipCallback(true);
if (this !is CurrentPopup)
return true;
} while (++ScreenID < Screens.length);
if (ScreenID >= Screens.length)
return Finish();
}
@screen = Screens[ScreenID];
bool pressingKey = jjLocalPlayers[0].keyFire || jjLocalPlayers[0].keySelect || jjKey[1]; //left mouse button
if (!LastSelect && pressingKey) {
if (MaxCharactersToDraw > CharactersToDraw)
CharactersToDraw = MaxCharactersToDraw;
else if (TicksOnThisScreen > 60) { //can't skip screen for at least a second (70 ticks) [edited to 60, full second seems a little excessive in testing -GG]
playRandomMenuSample();
if (screen.choiceList.length != 0)
screen.choiceList[SelectedChoice].Effect();
if (screen.finishCallback !is null)
screen.finishCallback();
else if (screen.finishOrSkipCallback !is null)
screen.finishOrSkipCallback(false);
if (this !is CurrentPopup) //changed by a choice's effect
return true;
if (++ScreenID >= Screens.length)
return Finish();
else
if (Screens[ScreenID] !is null) StartScreen();
}
}
LastSelect = pressingKey;
if (screen.choiceCount > 1) {
pressingKey = jjLocalPlayers[0].keyUp || jjKey[0x26];
if (!LastUp && pressingKey) {
while (true) {
--SelectedChoice;
if (SelectedChoice < 0)
SelectedChoice = screen.choiceList.length - 1;
if (screen.choiceList[SelectedChoice].Style != HiddenAndDefaultChoice)
break;
}
}
LastUp = pressingKey;
pressingKey = jjLocalPlayers[0].keyDown || jjKey[0x28];
if (!LastDown && pressingKey) {
while (true) {
++SelectedChoice;
if (uint(SelectedChoice) >= screen.choiceList.length )
SelectedChoice = 0;
if (screen.choiceList[SelectedChoice].Style != HiddenAndDefaultChoice)
break;
}
}
LastDown = pressingKey;
}
CharactersToDraw += 1;
TicksOnThisScreen += 1;
return true;
}
bool Finish() const {
for (int i = 0; i < 4; i++) jjLocalPlayers[i].invincibility = storeInvincibility[i];
return false;
}
void drawH(jjCANVAS@ canvas, int x, int y, int w) {
canvas.drawRectangle(x, y, w, LineSize, StrokeColor);
canvas.drawRectangle(x, y + StrokeSize, w, FillSize, FillColor);
}
void drawV(jjCANVAS@ canvas, int x, int y, int h) {
canvas.drawRectangle(x, y, LineSize, h, StrokeColor);
canvas.drawRectangle(x + StrokeSize, y, FillSize, h, FillColor);
}
void Draw(jjCANVAS@ canvas) const override {
const int width = jjSubscreenWidth;
const int height = jjSubscreenHeight;
int y = 37;
drawH(canvas, 0, y, width);
int x = (width - LineSize) / 2;
drawV(canvas, x, 0, y + StrokeSize);
canvas.drawRectangle(0,0, x,y, MultiplyColors[(MultiplyColorsID + 0) % 7],SPRITE::BLEND_MULTIPLY, 255);
canvas.drawRectangle(x+LineSize,0, width-x-LineSize,y, MultiplyColors[(MultiplyColorsID + 1) % 7],SPRITE::BLEND_MULTIPLY, 255);
y = height - 53;
drawH(canvas, 0, y, width);
x /= 2;
y += StrokeSize + FillSize;
drawV(canvas, x, y, height - y);
canvas.drawRectangle(0, y + StrokeSize, x, height - y - StrokeSize, MultiplyColors[(MultiplyColorsID + 2) % 7],SPRITE::BLEND_MULTIPLY, 255);
canvas.drawRectangle(x + LineSize, y + StrokeSize, x * 3 - x - LineSize, height - y - StrokeSize, MultiplyColors[(MultiplyColorsID + 3) % 7],SPRITE::BLEND_MULTIPLY, 255);
x *= 3;
drawV(canvas, x, y, height - y);
canvas.drawRectangle(x + LineSize, y + StrokeSize, width - x - LineSize, height - y - StrokeSize, MultiplyColors[(MultiplyColorsID + 4) % 7],SPRITE::BLEND_MULTIPLY, 255);
y = 37 + StrokeSize + FillSize;
drawV(canvas, 43, y, height - 53 - y + StrokeSize);
drawV(canvas, width - 43 - LineSize, y, height - 53 - y + StrokeSize);
canvas.drawRectangle(0, y + StrokeSize, 43, height - 53 - y - StrokeSize, MultiplyColors[(MultiplyColorsID + 5) % 7],SPRITE::BLEND_MULTIPLY, 255);
canvas.drawRectangle(width - 43, y + StrokeSize, 43, height - 53 - y - StrokeSize, MultiplyColors[(MultiplyColorsID + 6) % 7],SPRITE::BLEND_MULTIPLY, 255);
canvas.drawRectangle(43 + LineSize, 37 + LineSize, width - (43 + LineSize) * 2, height - 37 - 53 - LineSize, 0,SPRITE::SHADOW);
const bool tooSmallForPortraits = width < 640 || height < 400;
if (!tooSmallForPortraits) {
const array<array<int>> portraitLocations = {{0,100}, {0,height-92}, {width-1,100}, {width-1,height-92}};
for (int cornerID = 0; cornerID < 4; ++cornerID) {
const array<int>@ corner = PortraitX[cornerID];
for (int portraitID = 0; portraitID < Portrait::_LAST; ++portraitID) {
const int xOffset = corner[portraitID];
if (xOffset != 180)
canvas.drawSprite(portraitLocations[cornerID][0] + (cornerID >= 2 ? xOffset : -xOffset), portraitLocations[cornerID][1], OrderPortraitSet, portraitID, cornerID & 1, (cornerID >= 2) ? -1 : 1);
}
}
}
const Screen@ screen = @Screens[ScreenID];
if (screen is null)
return;
for (uint boxID = 0; boxID < 2; ++boxID) {
if (screen.boxes[boxID] is null)
continue;
const Line@ box = screen.boxes[boxID];
const array<int>@ xs = box.LinesX;
const array<string>@ lines = box.WrappedLines;
if (lines.length != 0) {
canvas.drawRectangle(box.Left, box.Top, box.Width, box.Height, box.Color + 7);
canvas.drawRectangle(box.Left + StrokeSize, box.Top + StrokeSize, box.Width - StrokeSize*2, box.Height - StrokeSize*2, box.Color);
if (box.TailDirection != 0 && !tooSmallForPortraits)
canvas.drawSprite(box.TailDirection < 0 ? 0 : width-1, boxID == 0 ? 80 : height - 82, OrderPortraitSet, Portrait::_LAST, 0, boxID == 0 ? -box.TailDirection : (-box.TailDirection ^ 0x40), SPRITE::SINGLEHUE, box.Color);
int charactersRemaining = CharactersToDraw;
y = box.Top + 16;
for (uint lineID = 0; lineID < lines.length && charactersRemaining > 0; ++lineID) {
canvas.drawString(xs[lineID],y, lines[lineID].substr(0, charactersRemaining), OrderFontAnim, OrderTextAppearance,0, SPRITE::ALPHAMAP,0);
charactersRemaining -= lines[lineID].length;
y += 20;
}
}
}
if (screen.choiceCount != 0) {
int xPos = jjSubscreenWidth / 2;
int yPos = jjSubscreenHeight / 2;
const int choiceHeight = 35;
const int margin = 10;
xPos -= screen.maximumChoiceLength / 2 + StrokeSize;
yPos -= (screen.choiceCount * choiceHeight + screen.choiceCount - 1 * margin) / 2;
uint8 defaultColor;
switch (jjLocalPlayers[0].charCurr) {
case CHAR::SPAZ:
defaultColor = 24;
break;
case CHAR::LORI:
defaultColor = 40;
break;
default:
defaultColor = 16;
break;
}
for (uint i = 0; i < screen.choiceList.length; ++i) {
const Choice@ choice = screen.choiceList[i];
if (choice.Style == HiddenAndDefaultChoice)
continue;
uint8 color = choice.Color;
if (color == 0)
color = defaultColor;
canvas.drawRectangle(xPos, yPos, screen.maximumChoiceLength, choiceHeight, color | 7);
canvas.drawRectangle(xPos + StrokeSize, yPos + StrokeSize, screen.maximumChoiceLength - StrokeSize*2, choiceHeight - StrokeSize*2, color);
canvas.drawString(xPos + 10, yPos + choiceHeight / 2, choice.Text.substr(0, CharactersToDraw), OrderFontAnim, OrderTextAppearance,0, SPRITE::ALPHAMAP,0);
if (i == uint(SelectedChoice))
canvas.drawSpriteFromCurFrame(xPos - 15, yPos + 15, OrderPointingFingerCurFrame, mode: SPRITE::SINGLEHUE, param: defaultColor);
yPos += choiceHeight + margin;
}
}
}
}
void playRandomMenuSample() {
switch(jjRandom()%7) {
case 0: jjSamplePriority(SOUND::MENUSOUNDS_SELECT0); break;
case 1: jjSamplePriority(SOUND::MENUSOUNDS_SELECT1); break;
case 2: jjSamplePriority(SOUND::MENUSOUNDS_SELECT2); break;
case 3: jjSamplePriority(SOUND::MENUSOUNDS_SELECT3); break;
case 4: jjSamplePriority(SOUND::MENUSOUNDS_SELECT4); break;
case 5: jjSamplePriority(SOUND::MENUSOUNDS_SELECT5); break;
case 6: jjSamplePriority(SOUND::MENUSOUNDS_SELECT6); break;
}
}
bool PressingKeyToMakeCutsceneGoAway = false;
bool IsLevelWhereRunningIsAllowed = jjDifficulty < 2;
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.