Name | Author | Game Mode | Rating | |||||
---|---|---|---|---|---|---|---|---|
Level Guide | Stijn | Mutator | 10 |
//level guide mutator for jazz jackrabbit 2 + (https://jj2.plus)
//by stijn, 2017-2018 (https://www.jazz2online.com)
#pragma name "Level Guide"
//events that may be shown on minimap
array<int> interesting_events = {
OBJECT::BLASTERPOWERUP, OBJECT::BOUNCERPOWERUP, OBJECT::ICEPOWERUP, OBJECT::SEEKERPOWERUP, OBJECT::RFPOWERUP, OBJECT::TOASTERPOWERUP, OBJECT::GUN8POWERUP, OBJECT::GUN9POWERUP,
OBJECT::FIRESHIELD, OBJECT::WATERSHIELD, OBJECT::PLASMASHIELD, OBJECT::LASERSHIELD,
OBJECT::CARROT, OBJECT::FULLENERGY,
OBJECT::SILVERCOIN, OBJECT::GOLDCOIN,
OBJECT::ICEAMMO3, OBJECT::ICEAMMO15, AREA::MPSTART,
OBJECT::CTFBASE,
OBJECT::EOLPOST, AREA::WARPEOL, AREA::EOL, AREA::WARP, AREA::PATH
};
//arrays for convenience
array<int> power_ups = {OBJECT::BLASTERPOWERUP, OBJECT::BOUNCERPOWERUP, OBJECT::ICEPOWERUP, OBJECT::SEEKERPOWERUP, OBJECT::RFPOWERUP, OBJECT::TOASTERPOWERUP, OBJECT::GUN8POWERUP, OBJECT::GUN9POWERUP};
array<int> shields = {OBJECT::FIRESHIELD, OBJECT::WATERSHIELD, OBJECT::PLASMASHIELD, OBJECT::LASERSHIELD};
//objects that drop down to ground (i.e. monitors)
const array<uint> affected_by_gravity = {
OBJECT::BLASTERPOWERUP, OBJECT::BOUNCERPOWERUP, OBJECT::ICEPOWERUP, OBJECT::SEEKERPOWERUP, OBJECT::RFPOWERUP, OBJECT::TOASTERPOWERUP, OBJECT::GUN8POWERUP, OBJECT::GUN9POWERUP,
OBJECT::FIRESHIELD, OBJECT::WATERSHIELD, OBJECT::PLASMASHIELD, OBJECT::LASERSHIELD
};
//an interesting thing; e.g. a carrot, ctf base or powerup
class interesting_thing {
int xPos;
int yPos;
int width = 32;
int height = 32;
int eventID;
int objectID = -1;
int difficulty = 0;
int team = -1;
string typetext = "";
}
//a collection of interesting things
class cluster {
array<interesting_thing> things;
int originX;
int originY;
int levelX;
int levelY;
int width;
int height;
bool hover = false;
}
//feel free to tweak these
int panel_scale = 20; //size of icons on minimap
int panel_colour = 64; //base colour for map item panels
int panel_hover = 42; //hover overlay colour for map items
int ui_colour = 72; //base colour for most UI elements
int letterbox_huehue = 200; //transparency of letterbox in map view
//don't edit these
int pixels_per_tick = 750; //minimap pixels to render per gametick in background during precalc - auto-adjusted later
int precalc_done = 0; //0 = pre-init, 1 = init complete, rendering minimap, 2 = post-render setup, 3 = done
int precalc_x = 0;
int precalc_y = 0;
bool precalc_active = false;
float levelX;
float levelY;
int precalc_wait = 10;
bool show_guide = false;
bool show_hint = false;
bool advertised = false;
int page = 0;
int max_page = 0;
int highlight = 0;
int letterbox;
int letterbox_hue = 255;
bool camera_frozen;
string game_mode = "";
jjPIXELMAP@ minimap;
array<cluster> hotspots;
int customAnimID = 0;
string mode_mutator = "";
int minimap_width;
int minimap_height;
int minimap_x_offset = 0;
int minimap_y_offset = 0;
float minimap_x_step = 0;
float minimap_y_step = 0;
int layer4_start_x = 1024;
int layer4_end_x = 0;
int layer4_start_y = 1024;
int layer4_end_y = 0;
const int KEY_F1 = 0x70;
const int KEY_F2 = 0x71;
const int KEY_F5 = 0x74;
const int key_threshold = 20;
int key_decay = 0;
//some math functions for convenience
int max(int one, int two) { return (one > two) ? one : two; }
int min(int one, int two) { return (one < two) ? one : two; }
uint sum(array<int> arr) { uint result = 0; for(uint i = 0; i < arr.length(); i += 1) { result += arr[i]; }; return result; }
/**
* Handle key presses
*/
void onPlayerInput(jjPLAYER@ player) {
if(jjLocalPlayerCount > 1) {
if(jjKey[KEY_F1] && key_decay == 0) {
jjAlert('The level guide is not available in splitscreen mode.');
key_decay = 15;
}
return;
}
if(key_decay == 0) {
if(jjKey[KEY_F1]) { //toggle guide
if(!show_guide) {
if(page > max_page) {
page = max_page;
}
}
show_guide = !show_guide;
advertised = true;
key_decay = key_threshold;
} else if(jjKey[KEY_F2] && show_guide && page > 0) { //toggle arrows
show_hint = !show_hint;
key_decay = key_threshold;
} else if(jjKey[KEY_F5] && show_guide) { //go to minimap
page = 0;
} else if(show_guide && player.keyLeft) { //next page
if(page > 0) {
page -= 1;
} else {
highlight = 35;
page = max_page;
}
key_decay = key_threshold;
} else if(show_guide && player.keyRight) { //previous page
if(page < max_page) {
page += 1;
} else {
highlight = 35;
page = 0;
}
key_decay = key_threshold;
}
}
//mouse clicks: on minimap, send to clicked hotspot
if(show_guide && page == 0) {
for(uint i = 0; i < hotspots.length(); i += 1) {
cluster h = hotspots[i];
hotspots[i].hover = false;
if(jjMouseX >= h.originX && jjMouseX <= h.originX + h.width && jjMouseY >= h.originY && jjMouseY <= h.originY + h.height) {
if(jjKey[1] && key_decay == 0) {
page = i + 1;
key_decay = key_threshold;
} else {
hotspots[i].hover = true;
}
}
}
//on other pages, mouse = return to minimap
} else if(show_guide && jjKey[1] && page > 0 && key_decay == 0) {
page = 0;
key_decay = key_threshold;
}
//don't move player characters as long as guide is visible
if(show_guide) {
player.keyUp = player.keyDown = player.keyRight = player.keyLeft = player.keyFire = player.keyJump = false;
}
}
/**
* This is where the magic happens
*
* We're using onDrawAmmo because that way we can hide some potentially in-the-way HUD elements when showing the guide
*/
bool onDrawAmmo(jjPLAYER@ player, jjCANVAS@ screen) {
letterbox = int(jjResolutionWidth / 12.5); //pretty arbitrary, needs to be recalculated because resolution may change (though the minimap won't...)
bool panCamera = jjLocalPlayers[0].isSpectating;
if(!show_guide) {
if(camera_frozen) {
camera_frozen = false;
player.cameraUnfreeze(true);
}
return false;
}
if(precalc_done < 3) {
screen.drawRectangle(0, 0, jjResolutionWidth, jjResolutionHeight, ui_colour + 7);
drawStringCentered(screen, jjResolutionHeight / 2, "Rendering minimap...", STRING::SMALL, STRING::NORMAL);
if(precalc_wait > 0) {
precalc_wait -= 1;
} else {
precalc_pixels();
}
return true;
}
camera_frozen = true;
player.idle = 0;
//level map
if(page == 0) {
if(jjColorDepth == 16 && letterbox_hue < letterbox_huehue) {
letterbox_hue += 1;
}
player.cameraFreeze(true, true, true, false);
screen.drawRectangle(0, 0, jjResolutionWidth, jjResolutionHeight, ui_colour + 7, SPRITE::BLEND_NORMAL, letterbox_hue);
drawStringCentered(screen, 18, jjLevelName, STRING::LARGE, STRING::BOUNCE);
drawStringCentered(screen, letterbox + 5, "Level map", STRING::MEDIUM, STRING::BOUNCE);
screen.drawSprite(letterbox + minimap_x_offset, letterbox + 25 + minimap_y_offset, ANIM::CUSTOM[customAnimID], 0, 0);
//draw player character on map
if(!jjLocalPlayers[0].isSpectating) {
int playerX = int(((player.xPos - layer4_start_x * 32) / ((layer4_end_x - layer4_start_x) * 32)) * (minimap_width - minimap_x_offset - minimap_x_offset)) + letterbox + minimap_x_offset - 13;
int playerY = int(((player.yPos - layer4_start_y * 32) / ((layer4_end_y - layer4_start_y) * 32)) * (minimap_height - minimap_y_offset - minimap_y_offset)) + letterbox + minimap_y_offset + 25 - 13;
int anim;
switch(player.charCurr) {
case CHAR::JAZZ: anim = 3; break;
case CHAR::SPAZ: anim = (jjIsTSF) ? 5 : 4; break;
case CHAR::LORI: anim = 4; break;
case CHAR::BIRD: anim = 0; break;
case CHAR::FROG: anim = 2; break;
}
screen.drawRectangle(playerX - 4, playerY - 25, 1, 27, ui_colour + 4); //left
screen.drawRectangle(playerX + 21, playerY - 25, 1, 27, ui_colour + 4); //right
screen.drawRectangle(playerX - 3, playerY - 26, 24, 1, ui_colour + 4); //top
screen.drawRectangle(playerX - 3, playerY + 2, 24, 1, ui_colour + 4); //bottom
screen.drawRectangle(playerX- 3, playerY - 25, 24, 27, ui_colour + 3); //base
screen.drawRectangle(playerX- 2, playerY - 24, 22, 25, ui_colour + 2); //base
screen.drawRectangle(playerX- 1, playerY - 23, 20, 23, ui_colour + 1); //base
screen.drawResizedSprite(playerX, playerY, ANIM::FACES, anim, 0, 0.5, 0.55);
for(uint i = 0; i < hotspots.length(); i += 1) {
if(hotspots[i].hover) {
screen.drawRectangle(hotspots[i].originX, hotspots[i].originY, hotspots[i].width, hotspots[i].height, panel_hover, SPRITE::BLEND_OVERLAY, 255);
}
}
}
//level hotspots
} else {
if(letterbox_hue > 128 && jjColorDepth == 16) {
letterbox_hue -= 1;
}
//draw letterbox
screen.drawRectangle(0, letterbox + 25, letterbox, jjResolutionHeight - (letterbox * 2) - 25, ui_colour + 7, SPRITE::BLEND_NORMAL, letterbox_hue); //left
screen.drawRectangle(jjResolutionWidth - letterbox, letterbox + 25, letterbox, jjResolutionHeight - (letterbox * 2) - 25, ui_colour + 7, SPRITE::BLEND_NORMAL, letterbox_hue); //right
screen.drawRectangle(0, 0, jjResolutionWidth, letterbox + 25, ui_colour + 7, SPRITE::BLEND_NORMAL, letterbox_hue); //top
screen.drawRectangle(0, jjResolutionHeight - letterbox, jjResolutionWidth, letterbox, ui_colour + 7, SPRITE::BLEND_NORMAL, letterbox_hue); //bottom
drawStringCentered(screen, 18, jjLevelName, STRING::LARGE, STRING::BOUNCE);
drawStringCentered(screen, letterbox + 5, "Locations of interest", STRING::MEDIUM, STRING::BOUNCE);
array<interesting_thing> collection = hotspots[page - 1].things;
int hotspotX = 0;
int hotspotY = 0;
int hotspot_minX = 1024 * 32;
int hotspot_minY = 1024 * 32;
int hotspot_maxX = 0;
int hotspot_maxY = 0;
//determine what the hotspot contains and what its centre of gravity is
array<string> types = {};
for(uint i = 0; i < collection.length(); i += 1) {
hotspotX += int(collection[i].xPos);
hotspotY += int(collection[i].yPos);
hotspot_minX = min(hotspot_minX, int(collection[i].xPos) - collection[i].width / 2);
hotspot_minY = min(hotspot_minY, int(collection[i].yPos) - collection[i].height / 2);
hotspot_maxX = max(hotspot_maxX, int(collection[i].xPos) + collection[i].width / 2);
hotspot_maxY = max(hotspot_maxY, int(collection[i].yPos) + collection[i].height / 2);
if(types.find(collection[i].typetext) < 0) { types.insertLast(collection[i].typetext); }
}
//arrows pointing at hotspot
int margin = 32;
if(show_hint) {
screen.drawRotatedSprite(int(hotspot_minX - player.cameraX - margin), int(hotspot_minY - player.cameraY - margin) - 16, ANIM::FLAG, 0, 0, 850, 1.5, 1.5, SPRITE::SINGLEHUE, ui_colour);
screen.drawRotatedSprite(int(hotspot_maxX - player.cameraX + margin), int(hotspot_minY - player.cameraY - margin) - 16, ANIM::FLAG, 0, 0, 570, 1.5, 1.5, SPRITE::SINGLEHUE, ui_colour);
screen.drawRotatedSprite(int(hotspot_minX - player.cameraX - margin), int(hotspot_maxY - player.cameraY + margin) - 16, ANIM::FLAG, 0, 0, 50, 1.5, 1.5, SPRITE::SINGLEHUE, ui_colour);
screen.drawRotatedSprite(int(hotspot_maxX - player.cameraX + margin), int(hotspot_maxY - player.cameraY + margin) - 16, ANIM::FLAG, 0, 0, 320, 1.5, 1.5, SPRITE::SINGLEHUE, ui_colour);
//screen.drawRectangle(hotspot_minX - jjPlayers[0].cameraX, hotspot_minY - jjPlayers[0].cameraY, hotspot_maxX - hotspot_minX, hotspot_maxY - hotspot_minY, 16);
}
//pan camera to hotspot - panning is best because it gives a better sense of where in the level the hotspot exactly is
hotspotX /= collection.length();
hotspotY /= collection.length();
player.cameraFreeze(hotspotX, hotspotY, true, panCamera);
//draw box with text describing what can be found at the hotspot
string type_string = join(types, " / ");
int type_width = jjGetStringWidth(type_string, STRING::MEDIUM, STRING::NORMAL);
int rectWidth = type_width + 40;
int rectHeight = 32;
int rectX = (jjResolutionWidth / 2) - (type_width / 2) - 20;
int rectY = jjResolutionHeight - letterbox - rectHeight - 20;
screen.drawRectangle(rectX, rectY, rectWidth, rectHeight, ui_colour + 7, SPRITE::TRANSLUCENT);
int colour = ui_colour + 6;
for(int i = 0; i < 5; i += 1) {
if(i < 3) {
colour -= 1;
} else {
colour += 1;
}
screen.drawRectangle(rectX - i, rectY - i, rectWidth + (2 * i), 1, colour); //top
screen.drawRectangle(rectX - i, rectY + rectHeight + i, rectWidth + (2 * i), 1, colour); //bottom
screen.drawRectangle(rectX - i, rectY - i, 1, rectHeight + (2 * i), colour); //left
screen.drawRectangle(rectX + rectWidth + i, rectY - i, 1, rectHeight + 1 + (2 * i), colour); //right
}
drawStringCentered(screen, rectY + 15, type_string, STRING::MEDIUM, STRING::NORMAL);
}
//other hud texts
screen.drawString(jjResolutionWidth - letterbox - 90, jjResolutionHeight - letterbox + 16, "Page " + (page + 1) + "/" + (max_page + 1), STRING::SMALL, STRING::NORMAL);
if(highlight > 0) {
drawStringCentered(screen, letterbox + 25 + 16, "|||||||Press |F1||||||| to close the level guide", STRING::SMALL, STRING::NORMAL);
highlight -= 1;
} else {
drawStringCentered(screen, letterbox + 25 + 16, "Press |F1||||| to close the level guide", STRING::SMALL, STRING::NORMAL);
}
if(page > 0) {
drawStringCentered(screen, letterbox + 25 + 32, "Press |F2||||| for location hints, or |||click||||| to return to the map", STRING::SMALL, STRING::NORMAL);
} else {
drawStringCentered(screen, letterbox + 25 + 32, "|Click||||| any icon to inspect location", STRING::SMALL, STRING::NORMAL);
}
//little mouse cursor
screen.drawRectangle(jjMouseX, jjMouseY, 2, 2, 64);
return false;
}
/**
* Hide score while guide is visible
*
* This only really works in SP and Coop, but anyway, better than nothing
*/
bool onDrawScore(jjPLAYER@ player, jjCANVAS@ canvas) {
return show_guide;
}
/**
* Hide health while guide is visible
*/
bool onDrawHealth(jjPLAYER@ player, jjCANVAS@ canvas) {
return show_guide;
}
/**
* Detect mutators
*
* Right now only looks for compatible Bank Robbery mutators, but could be extended to look
* for other custom modes as well.
*/
void onLevelLoad() {
jjPUBLICINTERFACE@ bankmut = jjGetPublicInterface("bank.mut");
jjPUBLICINTERFACE@ boxlessmut = jjGetPublicInterface("boxlessbr.mut");
jjPUBLICINTERFACE@ onslaughtmut = jjGetPublicInterface("onslaught.mut");
if(bankmut !is null || boxlessmut !is null) { //bank robbery mutator is present
mode_mutator = "br";
}
if(onslaughtmut !is null) { //bank robbery mutator is present
mode_mutator = "ons";
}
}
/**
* Main hook
*
* Does very little; most stuff happens in onDrawAmmo. This adjsuts some decaying variables,
* manages precalc and advertises the level guide when everything's loaded.
*/
void onMain() {
if(key_decay > 0) {
key_decay -= 1;
}
//this tries to balance the load of rendering the minimap in the background in a very rudimentary way
if(jjGameTicks % 35 == 1 && jjFPS > 0) {
pixels_per_tick = jjFPS * 10;
}
if(precalc_done == 1) {
precalc_pixels(pixels_per_tick);
}
if(jjColorDepth == 8) {
letterbox_hue = 255; //huehue
}
//re-render minimap if game mode changes - since different objects may now be of interest
string current_mode = jjGameCustom + "" + jjGameMode;
if(current_mode != game_mode) {
game_mode = current_mode;
find_hotspots();
precalc_done = 0;
}
if(precalc_done == 0) {
precalc_init();
}
//announce availability of guide when level begins
if(precalc_done == 3 && !advertised && jjLocalPlayerCount == 1) {
advertised = true;
jjAlert("A level guide is available. Press ||F1|||||| to view.");
}
}
/**
* Draw string centered on canvas
*/
void drawStringCentered(jjCANVAS@ screen, int yPos, string text, STRING::Size size, STRING::Mode style) {
int width = jjGetStringWidth(join(text.split("|"), ""), size, style); //ghetto string replace
screen.drawString((jjResolutionWidth / 2) - (width / 2), yPos, text, size, style);
}
/**
* Pre-render minimap
*
* Rendering the minimap each frame is far too expensive, so sacrifice about a second of gameplay
* to render it on the first run of the level guide. Also does some other misc setup such as
* allocating space for sprites and determining letterbox size.
*/
void precalc_init() {
if(precalc_done > 0) {
return;
}
precalc_x = 0;
precalc_y = 0;
advertised = false;
letterbox = int(jjResolutionWidth / 12.5); //pretty arbitrary
letterbox_hue = (jjColorDepth == 8) ? 255 : letterbox_huehue;
//allocate space to save the minimap etc as sprites
while (jjAnimSets[ANIM::CUSTOM[customAnimID]].firstAnim != 0) ++customAnimID;
array<uint> customFrames = {3};
jjAnimSets[ANIM::CUSTOM[customAnimID]].allocate(customFrames);
//calculate drawable area for minimap - ignore empty space surrounding the level
minimap_width = jjResolutionWidth - (letterbox * 2);
minimap_height = jjResolutionHeight - (letterbox * 2) - 25;
for(int y = 0; y < jjLayers[4].height; y += 1) {
for(int x = 0; x < jjLayers[4].width; x += 1) {
if(jjLayers[4].tileGet(x, y) != 0) {
layer4_start_x = min(layer4_start_x, x);
layer4_start_y = min(layer4_start_y, y);
layer4_end_x = max(layer4_end_x, x);
layer4_end_y = max(layer4_end_y, y);
}
}
}
//scale full level to minimap size
int width = layer4_end_x - layer4_start_x + 1;
int height = layer4_end_y - layer4_start_y + 1;
float aspect = float(width) / float(height);
float minimap_aspect = float(minimap_width) / float(minimap_height);
int half_width = int(minimap_width / 2);
if(aspect < minimap_aspect) {
minimap_y_offset = 0;
minimap_x_offset = int((minimap_width - (minimap_height * aspect)) / 2);
} else {
minimap_x_offset = 0;
minimap_y_offset = int((minimap_height - (minimap_height * (minimap_aspect / aspect))) / 2);
}
minimap_x_step = float(width * 32) / float(minimap_width - (minimap_x_offset * 2.0));
minimap_y_step = float(height * 32) / float(minimap_height - (minimap_y_offset * 2.0));
//render minimap
@minimap = jjPIXELMAP(uint(minimap_width - (minimap_x_offset * 2.0)), uint(minimap_height - (minimap_y_offset * 2.0)));
levelX = layer4_start_x * 32;
levelY = layer4_start_y * 32;
precalc_done = 1;
}
/**
* Render minimap
*
* Takes a number of pixels as an argument; only so many pixels will be rendered. This
* allows JJ2 to render the minimap in the background while gameplay continues, instead
* of the game hanging while rendering. Omitting the argument renders the full minimap
* (or what is left to be rendered)
*/
void precalc_pixels(int pixels = 0) {
if(precalc_active || precalc_done != 1) {
return;
}
precalc_done = 1;
precalc_active = true;
int pixels_done = 0;
if(pixels == 0) {
pixels = minimap.height * minimap.width + 1;
}
for(; precalc_y < int(minimap.height); precalc_y += 1) {
for(; precalc_x < int(minimap.width); precalc_x += 1) {
pixels_done += 1;
int tileX = int(levelX / 32);
int tileY = int(levelY / 32);
for(int l = 7; l > 0; l -= 1) {
if(jjLayers[l].xSpeed == 1 && jjLayers[l].ySpeed == 1) {
if((tileX > jjLayers[l].width && !jjLayers[l].tileWidth) || tileY > jjLayers[l].height && !jjLayers[l].tileHeight) {
continue;
}
int tileID = jjLayers[l].tileGet(tileX % jjLayers[l].width, tileY % jjLayers[l].height);
if(tileID > 0) {
jjPIXELMAP tile(tileID);
int color = tile[int(levelX % 32), int(levelY % 32)];
if(color > 0) {
minimap[precalc_x, precalc_y] = color;
}
}
}
}
levelX += minimap_x_step;
if(pixels_done >= pixels) {
precalc_x += 1;
precalc_active = false;
return;
}
}
levelX = layer4_start_x * 32;
levelY += minimap_y_step;
precalc_x = 0;
}
precalc_done = 2;
finish_precalc();
precalc_active = false;
}
/**
* Finish precalc
*
* Renders item icons to the minimap and sets up some misc stuff
*/
void finish_precalc() {
//show location of important items on the minimap
find_hotspots();
//icons for each category and corresponding sprites
array<int> sprites = {ANIM::FLAG, ANIM::FLAG, ANIM::PICKUPS, ANIM::PLUS_COMMON, ANIM::PICKUPS, ANIM::AMMO, ANIM::PICKUPS, ANIM::PICKUPS, //base/control point, health, unused, coin, ice ammo, level exit, jail
ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, //powerups
ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PLUS_COMMON}; //shields
array<int> anims = {4, 8, 21, 2, 37, 28, 28, 52,
60, 61, 62, 63, 64, 65, 66, 67,
31, 10, 51, 2};
if(jjLocalPlayers[0].charCurr == CHAR::SPAZ) { anims[8] = 83; } //spaz blaster monitor
if(jjLocalPlayers[0].charCurr == CHAR::LORI) { sprites[8] = ANIM::PLUS_COMMON; anims[8] = 5; } //lori blaster monitor
for(uint i = 0; i < hotspots.length(); i += 1) {
array<int> has_category = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
//first determine where on the minimap the hotspot is and what kind of items it contains
int hotspot_x = 0;
int hotspot_y = 0;
for(uint j = 0; j < hotspots[i].things.length(); j += 1) {
interesting_thing t = hotspots[i].things[j];
hotspot_x += t.xPos;
hotspot_y += t.yPos;
if(has_category[0] == 0) has_category[0] = ((t.eventID == OBJECT::CTFBASE && t.team != 1) || (t.eventID == AREA::PATH && t.team == 0) || (jjGameCustom == GAME::DOM && (t.difficulty == 1 || t.difficulty == 2)) || t.eventID == AREA::WARP) ? 1 : 0;
if(has_category[1] == 0) has_category[1] = ((t.eventID == OBJECT::CTFBASE || t.eventID == AREA::PATH) && t.team == 1) ? 1 : 0;
if(has_category[2] == 0) has_category[2] = (t.eventID == OBJECT::CARROT || t.eventID == OBJECT::FULLENERGY) ? 1 : 0;
if(has_category[4] == 0) has_category[4] = (t.eventID == OBJECT::SILVERCOIN || t.eventID == OBJECT::GOLDCOIN) ? 1 : 0;
if(has_category[5] == 0) has_category[5] = (mode_mutator == "ons" || jjGameCustom == GAME::JB) && (t.eventID == OBJECT::ICEAMMO3 || t.eventID == OBJECT::ICEAMMO15 || t.eventID == OBJECT::ICEPOWERUP) ? 1 : 0;
if(has_category[6] == 0) has_category[6] = (t.eventID == OBJECT::EOLPOST || t.eventID == AREA::EOL || t.eventID == AREA::WARPEOL) ? 1 : 0;
if(has_category[7] == 0) has_category[7] = (t.eventID == AREA::MPSTART || (t.eventID == AREA::PATH && t.team < 0)) ? 1 : 0;
for(int k = 8; k < 16; k += 1) {
if(has_category[k] == 0) has_category[k] = (t.eventID == power_ups[k - 8]) ? 1 : 0;
}
for(int k = 16; k < 20; k += 1) {
if(has_category[k] == 0) has_category[k] = (t.eventID == shields[k - 16]) ? 1 : 0;
}
}
hotspot_x /= hotspots[i].things.length();
hotspot_y /= hotspots[i].things.length();
hotspot_x = int((hotspot_x - (layer4_start_x * 32)) / minimap_x_step);
hotspot_y = int((hotspot_y - (layer4_start_y * 32)) / minimap_y_step);
hotspots[i].levelX = hotspot_x;
hotspots[i].levelY = hotspot_y;
//then draw a small panel with icons for each type present
int panel_width = int(panel_scale * 0.75);
int panel_height = int(panel_scale * 1.5);
panel_width += sum(has_category) * int(panel_scale * 1.25);
hotspots[i].width = panel_width;
hotspots[i].height = panel_height;
//offsets - make sure the panel is within bounds
int xo = max(0, hotspot_x - (panel_width / 2));
int yo = max(0, hotspot_y - (panel_height / 2));
xo = min(xo, minimap_width - (minimap_x_offset * 2) - panel_width);
yo = min(yo, minimap_height - (minimap_y_offset * 2) - panel_height);
//draw it
for(int x = 0; x < panel_width; x += 1) {
for(int y = 0; y < panel_height; y += 1) {
if((x == 0 && y == 0) || (x == 0 && y == panel_height - 1) || (x == panel_width - 1 && y == 0) || (x == panel_width - 1 && y == panel_height - 1)) {
continue; //rounded corners
}
int colour = panel_colour;
if(y <= 2 || y >= (panel_height - 3) || x <= 2 || x >= (panel_width - 3)) {
colour += 1;
}
if(y <= 1 || y >= (panel_height - 2) || x <= 1 || x >= (panel_width - 2)) {
colour += 1;
}
if(y <= 0 || y >= (panel_height - 1) || x <= 0 || x >= (panel_width - 1)) {
colour += 1;
}
minimap[x + xo, y + yo] = colour;
}
}
//then draw the icons on the panel
for(uint j = 0; j < has_category.length(); j += 1) {
if(has_category[j] == 1) {
int sprite_offset = (panel_scale / 2);
for(uint k = 0; k < j; k += 1) {
if(has_category[k] == 1) {
sprite_offset += int(panel_scale * 1.25);
}
}
jjPIXELMAP sprite(jjAnimFrames[jjAnimations[jjAnimSets[sprites[j]].firstAnim + anims[j]].firstFrame]);
for(int x = 0; x < panel_scale; x += 1) {
for(int y = 0; y < panel_scale; y += 1) {
int ref_x = int(x * float(sprite.width) / float(panel_scale));
int ref_y = int(y * float(sprite.height) / float(panel_scale));
if(sprite[ref_x, ref_y] != 0) {
minimap[x + sprite_offset + xo, y + yo + int(panel_scale * 0.25)] = sprite[ref_x, ref_y];
}
}
}
}
}
hotspots[i].originX = xo + letterbox + minimap_x_offset;
hotspots[i].originY = yo + letterbox + 25 + minimap_y_offset;
}
jjANIMFRAME@ minimapframe = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::CUSTOM[customAnimID]].firstAnim].firstFrame];
minimap.save(minimapframe);
precalc_wait = 10;
precalc_done = 3;
}
/**
* Find hotspots
*
* Looks for objects that are of interest (powerups, health, shields, bases) and
* saves a list of locations; objects that are near to each other are clustered
*/
void find_hotspots() {
//see which objects are worth showing in the guide
array<interesting_thing> objects_found = {};
hotspots.removeRange(0, hotspots.length());
for(int i = 1; i < jjObjectCount; i += 1) {
jjOBJ@ obj = jjObjects[i];
int e = obj.eventID;
if(e == OBJECT::GENERATOR) {
e = obj.var[3];
}
int team = -1;
if(e == OBJECT::CTFBASE) {
team = obj.var[1];
}
if(obj.isActive && is_interesting(e)) {
interesting_thing thing();
thing.xPos = int(obj.xOrg);
thing.yPos = int(obj.yOrg);
thing.eventID = e;
thing.objectID = i;
thing.team = team;
objects_found.insertLast(thing);
}
}
//we may get duplicates of what we found in jjObjects here, but that doesn't matter
//since the minimap looks at types of objects, not the amount
for(int y = 0; y < jjLayers[4].height; y += 1) {
for(int x = 0; x < jjLayers[4].width; x += 1) {
int eventID = jjEventGet(x, y);
int difficulty = jjParameterGet(x, y, -4, 2);
if(eventID == OBJECT::GENERATOR) {
eventID = jjParameterGet(x, y, 0, 8);
}
int team = -1;
if(eventID == OBJECT::CTFBASE) {
team = jjParameterGet(x, y, 0, 1);
}
if(eventID == AREA::PATH) {
int speed = jjParameterGet(x, y, 0, 6);
if(speed == 0) {
team = 0;
} else if(speed == 21) {
team = 1;
}
}
if(eventID > 0 && is_interesting(eventID, difficulty)) {
interesting_thing thing();
thing.xPos = x * 32 + 16;
thing.yPos = y * 32 + 16;
thing.eventID = eventID;
thing.difficulty = difficulty;
thing.team = team;
objects_found.insertLast(thing);
}
}
}
//check which of those are actually relevant to the current game mode
for(int i = objects_found.length() - 1; i >= 0; i -= 1) {
if(!is_relevant(objects_found[i])) {
objects_found.removeAt(i);
} else {
if(affected_by_gravity.find(objects_found[i].eventID) >= 0) {
objects_found[i].yPos += jjMaskedTopVLine(objects_found[i].xPos, objects_found[i].yPos, jjLayers[4].height * 32);
}
objects_found[i].typetext = get_typetext(objects_found[i]);
if(objects_found[i].eventID == OBJECT::CTFBASE) {
if(jjGameCustom == GAME::DOM) {
objects_found[i].width = objects_found[i].height = 64;
} else {
objects_found[i].width = 128;
objects_found[i].height = 96;
}
}
}
}
//cluster nearby objects into hotspots
while(objects_found.length() > 0) {
cluster new_collection;
new_collection.things.insertLast(objects_found[0]);
objects_found.removeAt(0);
for(int i = objects_found.length() - 1; i >= 0; i -= 1) {
for(uint j = 0; j < new_collection.things.length(); j += 1) {
if(objects_are_near(new_collection.things[j], objects_found[i])) {
new_collection.things.insertLast(objects_found[i]);
objects_found.removeAt(i);
break;
}
}
}
hotspots.insertLast(new_collection);
}
max_page = hotspots.length();
}
/**
* Check if event is interesting
*
* "Interesting" here means that it may potentially be shown; whether it will actually
* be shown to players depends on other factors
*/
bool is_interesting(int e, int difficulty = 0) {
if((difficulty == 1 || difficulty == 2) && jjGameCustom == GAME::DOM) {
return true;
}
return interesting_events.find(e) >= 0;
}
/**
* Check if thing is *relevant*
*
* Relevant means it's useful to point out in the current game mode; thus relevant
* things are a subset of interesting things
*/
bool is_relevant(interesting_thing t) {
int e = t.eventID;
if(e == AREA::WARP) {
return (jjGameCustom == GAME::FR && t.difficulty == 1);
}
if(e == OBJECT::EOLPOST || e == AREA::EOL || e == AREA::WARPEOL) {
return (jjGameCustom == GAME::NOCUSTOM && (jjGameMode == GAME::COOP || jjGameMode == GAME::TREASURE));
}
if(e == OBJECT::SILVERCOIN || e == OBJECT::GOLDCOIN) {
return (mode_mutator == "br"); //detect bank robbery here, somehow
}
if(e == AREA::PATH) {
return (mode_mutator == "ons");
}
if(e == OBJECT::ICEAMMO3 || e == OBJECT::ICEAMMO15) {
return (jjGameCustom == GAME::JB || mode_mutator == "ons");
}
if(e == AREA::MPSTART) {
return (jjGameCustom == GAME::JB);
}
if(e == AREA::TEXT) {
return (jjGameCustom == GAME::DOM && (t.difficulty == 1 || t.difficulty == 2));
}
if(e == OBJECT::CTFBASE) {
return (jjGameCustom == GAME::NOCUSTOM && jjGameMode == GAME::CTF) || (jjGameCustom == GAME::DOM || jjGameCustom == GAME::FR || jjGameCustom == GAME::DCTF || jjGameCustom == GAME::HEAD);
}
if(jjGameCustom == GAME::DOM && (t.difficulty == 1 || t.difficulty == 2)) {
return true;
}
return interesting_events.find(e) >= 0;
}
/**
* Get text describing type of thing
*/
string get_typetext(interesting_thing t) {
int e = t.eventID;
if(e == AREA::WARP) {
return "Flag";
}
if(e == OBJECT::EOLPOST || e == AREA::EOL || e == AREA::WARPEOL) {
return "Exit";
}
if(e == OBJECT::SILVERCOIN || e == OBJECT::GOLDCOIN) {
return "Coins";
}
if(e == OBJECT::ICEAMMO3 || e == OBJECT::ICEAMMO15) {
return (mode_mutator == "ons") ? "Node builder ammo" : "Freezer ammo";
}
if(e == AREA::MPSTART) {
return "Jail";
}
if(e == OBJECT::CTFBASE) {
return (jjGameCustom == GAME::DOM) ? "Control point" : "Base";
}
if(e == AREA::PATH) {
return "Control point";
}
if(power_ups.find(e) >= 0) {
return "PowerUp";
}
if(e == OBJECT::CARROT || e == OBJECT::FULLENERGY) {
return "Health";
}
if(jjGameCustom == GAME::DOM && (t.difficulty == 1 || t.difficulty == 2)) {
return "Control point";
}
if(e == OBJECT::FIRESHIELD || e == OBJECT::WATERSHIELD || e == OBJECT::PLASMASHIELD || e == OBJECT::LASERSHIELD) {
return "Shield";
}
return "MYSTERY OBJECT";
}
/**
* Check if two objects are near each other
*
* Radius within which object must be is half the screen; vary if objects don't cluster easily enough
*/
bool objects_are_near(interesting_thing one, interesting_thing two) {
float xRadius = jjResolutionWidth / 4;
float yRadius = jjResolutionHeight / 4;
float xMin = one.xPos - xRadius;
float xMax = one.xPos + xRadius;
float yMin = one.yPos - yRadius;
float yMax = one.yPos + yRadius;
return (two.xPos > xMin && two.xPos < xMax && two.yPos > yMin && two.yPos < yMax);
}
/**
* Minimal public interface support just so other mutators can detect this one's presence
*/
shared interface PublicInterface : jjPUBLICINTERFACE { }
class PublicClass : PublicInterface { string getVersion() const { return "1.1"; } }
PublicClass publicInstance;
PublicClass@ onGetPublicInterface() { return publicInstance; }
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.