Downloads containing guide.mut

Downloads
Name Author Game Mode Rating
JJ2+ Only: Level GuideFeatured Download Stijn Mutator 10 Download file

File preview

  1. //level guide mutator for jazz jackrabbit 2 + (https://jj2.plus)
  2. //by stijn, 2017-2018 (https://www.jazz2online.com)
  3. #pragma name "Level Guide"
  4.  
  5. //events that may be shown on minimap
  6. array<int> interesting_events = {
  7.   OBJECT::BLASTERPOWERUP, OBJECT::BOUNCERPOWERUP, OBJECT::ICEPOWERUP, OBJECT::SEEKERPOWERUP, OBJECT::RFPOWERUP, OBJECT::TOASTERPOWERUP, OBJECT::GUN8POWERUP, OBJECT::GUN9POWERUP,
  8.   OBJECT::FIRESHIELD,  OBJECT::WATERSHIELD, OBJECT::PLASMASHIELD, OBJECT::LASERSHIELD,
  9.   OBJECT::CARROT, OBJECT::FULLENERGY,
  10.   OBJECT::SILVERCOIN, OBJECT::GOLDCOIN,
  11.   OBJECT::ICEAMMO3, OBJECT::ICEAMMO15, AREA::MPSTART,
  12.   OBJECT::CTFBASE,
  13.   OBJECT::EOLPOST, AREA::WARPEOL, AREA::EOL, AREA::WARP, AREA::PATH
  14. };
  15.  
  16. //arrays for convenience
  17. array<int> power_ups = {OBJECT::BLASTERPOWERUP, OBJECT::BOUNCERPOWERUP, OBJECT::ICEPOWERUP, OBJECT::SEEKERPOWERUP, OBJECT::RFPOWERUP, OBJECT::TOASTERPOWERUP, OBJECT::GUN8POWERUP, OBJECT::GUN9POWERUP};
  18. array<int> shields = {OBJECT::FIRESHIELD,  OBJECT::WATERSHIELD, OBJECT::PLASMASHIELD, OBJECT::LASERSHIELD};
  19.  
  20. //objects that drop down to ground (i.e. monitors)
  21. const array<uint> affected_by_gravity = {
  22.         OBJECT::BLASTERPOWERUP, OBJECT::BOUNCERPOWERUP, OBJECT::ICEPOWERUP, OBJECT::SEEKERPOWERUP, OBJECT::RFPOWERUP, OBJECT::TOASTERPOWERUP, OBJECT::GUN8POWERUP, OBJECT::GUN9POWERUP,
  23.   OBJECT::FIRESHIELD,  OBJECT::WATERSHIELD, OBJECT::PLASMASHIELD, OBJECT::LASERSHIELD
  24. };
  25.  
  26. //an interesting thing; e.g. a carrot, ctf base or powerup
  27. class interesting_thing {
  28.   int xPos;
  29.   int yPos;
  30.   int width = 32;
  31.   int height = 32;
  32.   int eventID;
  33.   int objectID = -1;
  34.   int difficulty = 0;
  35.   int team = -1;
  36.   string typetext = "";
  37. }
  38.  
  39. //a collection of interesting things
  40. class cluster {
  41.   array<interesting_thing> things;
  42.   int originX;
  43.   int originY;
  44.   int levelX;
  45.   int levelY;
  46.   int width;
  47.   int height;
  48.   bool hover = false;
  49. }
  50.  
  51. //feel free to tweak these
  52. int panel_scale = 20;       //size of icons on minimap
  53. int panel_colour = 64;      //base colour for map item panels
  54. int panel_hover = 42;       //hover overlay colour for map items
  55. int ui_colour = 72;         //base colour for most UI elements
  56. int letterbox_huehue = 200; //transparency of letterbox in map view
  57.  
  58. //don't edit these
  59. int pixels_per_tick = 750;  //minimap pixels to render per gametick in background during precalc - auto-adjusted later
  60. int precalc_done = 0;       //0 = pre-init, 1 = init complete, rendering minimap, 2 = post-render setup, 3 = done
  61. int precalc_x = 0;
  62. int precalc_y = 0;
  63. bool precalc_active = false;
  64. float levelX;
  65. float levelY;
  66. int precalc_wait = 10;
  67.  
  68. bool show_guide = false;
  69. bool show_hint = false;
  70. bool advertised = false;
  71. int page = 0;
  72. int max_page = 0;
  73. int highlight = 0;
  74. int letterbox;
  75. int letterbox_hue = 255;
  76. bool camera_frozen;
  77. string game_mode = "";
  78.  
  79. jjPIXELMAP@ minimap;
  80. array<cluster> hotspots;
  81. int customAnimID = 0;
  82. string mode_mutator = "";
  83.  
  84. int minimap_width;
  85. int minimap_height;
  86. int minimap_x_offset = 0;
  87. int minimap_y_offset = 0;
  88. float minimap_x_step = 0;
  89. float minimap_y_step = 0;
  90. int layer4_start_x = 1024;
  91. int layer4_end_x = 0;
  92. int layer4_start_y = 1024;
  93. int layer4_end_y = 0;
  94.  
  95. const int KEY_F1 = 0x70;
  96. const int KEY_F2 = 0x71;
  97. const int KEY_F5 = 0x74;
  98. const int key_threshold = 20;
  99. int key_decay = 0;
  100.  
  101. //some math functions for convenience
  102. int max(int one, int two) { return (one > two) ? one : two; }
  103. int min(int one, int two) { return (one < two) ? one : two; }
  104. uint sum(array<int> arr) { uint result = 0; for(uint i = 0; i < arr.length(); i += 1) { result += arr[i]; }; return result; }
  105.  
  106. /**
  107.  * Handle key presses
  108.  */
  109. void onPlayerInput(jjPLAYER@ player) {
  110.   if(jjLocalPlayerCount > 1) {
  111.     if(jjKey[KEY_F1] && key_decay == 0) {
  112.       jjAlert('The level guide is not available in splitscreen mode.');
  113.       key_decay = 15;
  114.     }
  115.     return;
  116.   }
  117.  
  118.   if(key_decay == 0) {
  119.     if(jjKey[KEY_F1]) { //toggle guide
  120.       if(!show_guide) {
  121.         if(page > max_page) {
  122.           page = max_page;
  123.         }
  124.       }
  125.       show_guide = !show_guide;
  126.       advertised = true;
  127.       key_decay = key_threshold;
  128.     } else if(jjKey[KEY_F2] && show_guide && page > 0) { //toggle arrows
  129.       show_hint = !show_hint;
  130.       key_decay = key_threshold;
  131.     } else if(jjKey[KEY_F5] && show_guide) { //go to minimap
  132.       page = 0;
  133.     } else if(show_guide && player.keyLeft) { //next page
  134.       if(page > 0) {
  135.         page -= 1;
  136.       } else {
  137.         highlight = 35;
  138.         page = max_page;
  139.       }
  140.       key_decay = key_threshold;
  141.     } else if(show_guide && player.keyRight) { //previous page
  142.       if(page < max_page) {
  143.         page += 1;
  144.       } else {
  145.         highlight = 35;
  146.         page = 0;
  147.       }
  148.       key_decay = key_threshold;
  149.     }
  150.   }
  151.  
  152.   //mouse clicks: on minimap, send to clicked hotspot
  153.   if(show_guide && page == 0) {
  154.     for(uint i = 0; i < hotspots.length(); i += 1) {
  155.       cluster h = hotspots[i];
  156.       hotspots[i].hover = false;
  157.       if(jjMouseX >= h.originX && jjMouseX <= h.originX + h.width && jjMouseY >= h.originY && jjMouseY <= h.originY + h.height) {
  158.         if(jjKey[1] && key_decay == 0) {
  159.           page = i + 1;
  160.           key_decay = key_threshold;
  161.         } else {
  162.           hotspots[i].hover = true;
  163.         }
  164.       }
  165.     }
  166.   //on other pages, mouse = return to minimap
  167.   } else if(show_guide && jjKey[1] && page > 0 && key_decay == 0) {
  168.     page = 0;
  169.     key_decay = key_threshold;
  170.   }
  171.  
  172.   //don't move player characters as long as guide is visible
  173.   if(show_guide) {
  174.     player.keyUp = player.keyDown = player.keyRight = player.keyLeft = player.keyFire = player.keyJump = false;
  175.   }
  176. }
  177.  
  178. /**
  179.  * This is where the magic happens
  180.  *
  181.  * We're using onDrawAmmo because that way we can hide some potentially in-the-way HUD elements when showing the guide
  182.  */
  183. bool onDrawAmmo(jjPLAYER@ player, jjCANVAS@ screen) {
  184.   letterbox = int(jjResolutionWidth / 12.5); //pretty arbitrary, needs to be recalculated because resolution may change (though the minimap won't...)
  185.   bool panCamera = jjLocalPlayers[0].isSpectating;
  186.  
  187.   if(!show_guide) {
  188.     if(camera_frozen) {
  189.       camera_frozen = false;
  190.       player.cameraUnfreeze(true);  
  191.     }
  192.    
  193.     return false;
  194.   }
  195.  
  196.   if(precalc_done < 3) {
  197.     screen.drawRectangle(0, 0, jjResolutionWidth, jjResolutionHeight, ui_colour + 7);
  198.     drawStringCentered(screen, jjResolutionHeight / 2, "Rendering minimap...", STRING::SMALL, STRING::NORMAL);
  199.     if(precalc_wait > 0) {
  200.       precalc_wait -= 1;
  201.     } else {
  202.       precalc_pixels();
  203.     }
  204.     return true;
  205.   }
  206.  
  207.   camera_frozen = true;
  208.   player.idle = 0;
  209.  
  210.   //level map
  211.   if(page == 0) {
  212.     if(jjColorDepth == 16 && letterbox_hue < letterbox_huehue) {
  213.       letterbox_hue += 1;
  214.     }
  215.     player.cameraFreeze(true, true, true, false);
  216.     screen.drawRectangle(0, 0, jjResolutionWidth, jjResolutionHeight, ui_colour + 7, SPRITE::BLEND_NORMAL, letterbox_hue);
  217.     drawStringCentered(screen, 18, jjLevelName, STRING::LARGE, STRING::BOUNCE);
  218.     drawStringCentered(screen, letterbox + 5, "Level map", STRING::MEDIUM, STRING::BOUNCE);
  219.     screen.drawSprite(letterbox + minimap_x_offset, letterbox + 25 + minimap_y_offset, ANIM::CUSTOM[customAnimID], 0, 0);
  220.    
  221.     //draw player character on map
  222.     if(!jjLocalPlayers[0].isSpectating) {
  223.       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;
  224.       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;
  225.       int anim;
  226.       switch(player.charCurr) {
  227.         case CHAR::JAZZ: anim = 3; break;
  228.         case CHAR::SPAZ: anim = (jjIsTSF) ? 5 : 4; break;
  229.         case CHAR::LORI: anim = 4; break;
  230.         case CHAR::BIRD: anim = 0; break;
  231.         case CHAR::FROG: anim = 2; break;
  232.       }
  233.       screen.drawRectangle(playerX - 4, playerY - 25, 1, 27, ui_colour + 4); //left
  234.       screen.drawRectangle(playerX + 21, playerY - 25, 1, 27, ui_colour + 4); //right
  235.       screen.drawRectangle(playerX - 3, playerY - 26, 24, 1, ui_colour + 4); //top
  236.       screen.drawRectangle(playerX - 3, playerY + 2, 24, 1, ui_colour + 4); //bottom
  237.       screen.drawRectangle(playerX- 3, playerY - 25, 24, 27, ui_colour + 3); //base
  238.       screen.drawRectangle(playerX- 2, playerY - 24, 22, 25, ui_colour + 2); //base
  239.       screen.drawRectangle(playerX- 1, playerY - 23, 20, 23, ui_colour + 1); //base
  240.       screen.drawResizedSprite(playerX, playerY, ANIM::FACES, anim, 0, 0.5, 0.55);
  241.      
  242.       for(uint i = 0; i < hotspots.length(); i += 1) {
  243.         if(hotspots[i].hover) {
  244.           screen.drawRectangle(hotspots[i].originX, hotspots[i].originY, hotspots[i].width, hotspots[i].height, panel_hover, SPRITE::BLEND_OVERLAY, 255);
  245.         }
  246.       }
  247.     }
  248.    
  249.   //level hotspots
  250.   } else {
  251.     if(letterbox_hue > 128 && jjColorDepth == 16) {
  252.       letterbox_hue -= 1;
  253.     }
  254.    
  255.     //draw letterbox
  256.     screen.drawRectangle(0, letterbox + 25, letterbox, jjResolutionHeight - (letterbox * 2) - 25, ui_colour + 7, SPRITE::BLEND_NORMAL, letterbox_hue); //left
  257.     screen.drawRectangle(jjResolutionWidth - letterbox, letterbox + 25, letterbox, jjResolutionHeight - (letterbox * 2) - 25, ui_colour + 7, SPRITE::BLEND_NORMAL, letterbox_hue); //right
  258.     screen.drawRectangle(0, 0, jjResolutionWidth, letterbox + 25, ui_colour + 7, SPRITE::BLEND_NORMAL, letterbox_hue); //top
  259.     screen.drawRectangle(0, jjResolutionHeight - letterbox, jjResolutionWidth, letterbox, ui_colour + 7, SPRITE::BLEND_NORMAL, letterbox_hue); //bottom
  260.    
  261.     drawStringCentered(screen, 18, jjLevelName, STRING::LARGE, STRING::BOUNCE);
  262.     drawStringCentered(screen, letterbox + 5, "Locations of interest", STRING::MEDIUM, STRING::BOUNCE);
  263.      
  264.     array<interesting_thing> collection = hotspots[page - 1].things;
  265.     int hotspotX = 0;
  266.     int hotspotY = 0;
  267.     int hotspot_minX = 1024 * 32;
  268.     int hotspot_minY = 1024 * 32;
  269.     int hotspot_maxX = 0;
  270.     int hotspot_maxY = 0;
  271.    
  272.     //determine what the hotspot contains and what its centre of gravity is
  273.     array<string> types = {};
  274.     for(uint i = 0; i < collection.length(); i += 1) {
  275.       hotspotX += int(collection[i].xPos);
  276.       hotspotY += int(collection[i].yPos);      
  277.       hotspot_minX = min(hotspot_minX, int(collection[i].xPos) - collection[i].width / 2);
  278.       hotspot_minY = min(hotspot_minY, int(collection[i].yPos) - collection[i].height / 2);
  279.       hotspot_maxX = max(hotspot_maxX, int(collection[i].xPos) + collection[i].width / 2);
  280.       hotspot_maxY = max(hotspot_maxY, int(collection[i].yPos) + collection[i].height / 2);
  281.       if(types.find(collection[i].typetext) < 0) { types.insertLast(collection[i].typetext); }
  282.     }
  283.    
  284.     //arrows pointing at hotspot
  285.     int margin = 32;
  286.     if(show_hint) {
  287.       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);
  288.       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);
  289.       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);
  290.       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);
  291.       //screen.drawRectangle(hotspot_minX - jjPlayers[0].cameraX, hotspot_minY - jjPlayers[0].cameraY, hotspot_maxX - hotspot_minX, hotspot_maxY - hotspot_minY, 16);
  292.     }
  293.    
  294.     //pan camera to hotspot - panning is best because it gives a better sense of where in the level the hotspot exactly is
  295.     hotspotX /= collection.length();
  296.     hotspotY /= collection.length();
  297.     player.cameraFreeze(hotspotX, hotspotY, true, panCamera);
  298.    
  299.     //draw box with text describing what can be found at the hotspot
  300.     string type_string = join(types, " / ");
  301.     int type_width = jjGetStringWidth(type_string, STRING::MEDIUM, STRING::NORMAL);
  302.     int rectWidth = type_width + 40;
  303.     int rectHeight = 32;
  304.     int rectX = (jjResolutionWidth / 2) - (type_width / 2) - 20;
  305.     int rectY = jjResolutionHeight - letterbox - rectHeight - 20;
  306.      
  307.     screen.drawRectangle(rectX, rectY, rectWidth, rectHeight, ui_colour + 7, SPRITE::TRANSLUCENT);
  308.     int colour = ui_colour + 6;
  309.     for(int i = 0; i < 5; i += 1) {
  310.       if(i < 3) {
  311.         colour -= 1;
  312.       } else {
  313.         colour += 1;
  314.       }
  315.       screen.drawRectangle(rectX - i, rectY - i, rectWidth + (2 * i), 1, colour); //top
  316.       screen.drawRectangle(rectX - i, rectY + rectHeight + i, rectWidth + (2 * i), 1, colour); //bottom
  317.       screen.drawRectangle(rectX - i, rectY - i, 1, rectHeight + (2 * i), colour); //left
  318.       screen.drawRectangle(rectX + rectWidth + i, rectY - i, 1, rectHeight + 1 + (2 * i), colour); //right
  319.        
  320.     }
  321.     drawStringCentered(screen, rectY + 15, type_string, STRING::MEDIUM, STRING::NORMAL);
  322.   }
  323.  
  324.   //other hud texts
  325.   screen.drawString(jjResolutionWidth - letterbox - 90, jjResolutionHeight - letterbox + 16, "Page " + (page + 1) + "/" + (max_page + 1), STRING::SMALL, STRING::NORMAL);
  326.   if(highlight > 0) {
  327.     drawStringCentered(screen, letterbox + 25 + 16, "|||||||Press |F1||||||| to close the level guide", STRING::SMALL, STRING::NORMAL);
  328.     highlight -= 1;
  329.   } else {
  330.     drawStringCentered(screen, letterbox + 25 + 16, "Press |F1||||| to close the level guide", STRING::SMALL, STRING::NORMAL);
  331.   }
  332.   if(page > 0) {
  333.     drawStringCentered(screen, letterbox + 25 + 32, "Press |F2||||| for location hints, or |||click||||| to return to the map", STRING::SMALL, STRING::NORMAL);
  334.   } else {
  335.     drawStringCentered(screen, letterbox + 25 + 32, "|Click||||| any icon to inspect location", STRING::SMALL, STRING::NORMAL);
  336.   }
  337.  
  338.   //little mouse cursor
  339.   screen.drawRectangle(jjMouseX, jjMouseY, 2, 2, 64);
  340.   return false;
  341. }
  342.  
  343. /**
  344.  * Hide score while guide is visible
  345.  *
  346.  * This only really works in SP and Coop, but anyway, better than nothing
  347.  */
  348. bool onDrawScore(jjPLAYER@ player, jjCANVAS@ canvas) {
  349.   return show_guide;
  350. }
  351.  
  352. /**
  353.  * Hide health while guide is visible
  354.  */
  355. bool onDrawHealth(jjPLAYER@ player, jjCANVAS@ canvas) {
  356.   return show_guide;
  357. }
  358.  
  359. /**
  360.  * Detect mutators
  361.  *
  362.  * Right now only looks for compatible Bank Robbery mutators, but could be extended to look
  363.  * for other custom modes as well.
  364.  */
  365. void onLevelLoad() {
  366.   jjPUBLICINTERFACE@ bankmut = jjGetPublicInterface("bank.mut");
  367.   jjPUBLICINTERFACE@ boxlessmut = jjGetPublicInterface("boxlessbr.mut");
  368.   jjPUBLICINTERFACE@ onslaughtmut = jjGetPublicInterface("onslaught.mut");
  369.   if(bankmut !is null || boxlessmut !is null) { //bank robbery mutator is present
  370.     mode_mutator = "br";
  371.   }
  372.   if(onslaughtmut !is null) { //bank robbery mutator is present
  373.     mode_mutator = "ons";
  374.   }
  375. }
  376.  
  377. /**
  378.  * Main hook
  379.  *
  380.  * Does very little; most stuff happens in onDrawAmmo. This adjsuts some decaying variables,
  381.  * manages precalc and advertises the level guide when everything's loaded.
  382.  */
  383. void onMain() {
  384.   if(key_decay > 0) {
  385.     key_decay -= 1;
  386.   }
  387.  
  388.   //this tries to balance the load of rendering the minimap in the background in a very rudimentary way
  389.   if(jjGameTicks % 35 == 1 && jjFPS > 0) {
  390.     pixels_per_tick = jjFPS * 10;
  391.   }
  392.  
  393.   if(precalc_done == 1) {
  394.     precalc_pixels(pixels_per_tick);
  395.   }
  396.  
  397.   if(jjColorDepth == 8) {
  398.     letterbox_hue = 255; //huehue
  399.   }
  400.  
  401.   //re-render minimap if game mode changes - since different objects may now be of interest
  402.   string current_mode = jjGameCustom + "" + jjGameMode;
  403.   if(current_mode != game_mode) {
  404.     game_mode = current_mode;
  405.     find_hotspots();
  406.     precalc_done = 0;
  407.   }
  408.  
  409.   if(precalc_done == 0) {
  410.     precalc_init();
  411.   }
  412.        
  413.         //announce availability of guide when level begins
  414.   if(precalc_done == 3 && !advertised && jjLocalPlayerCount == 1) {
  415.     advertised = true;
  416.     jjAlert("A level guide is available. Press ||F1|||||| to view.");
  417.   }
  418. }
  419.  
  420. /**
  421.  * Draw string centered on canvas
  422.  */
  423. void drawStringCentered(jjCANVAS@ screen, int yPos, string text, STRING::Size size, STRING::Mode style) {
  424.   int width = jjGetStringWidth(join(text.split("|"), ""), size, style); //ghetto string replace
  425.   screen.drawString((jjResolutionWidth / 2) - (width / 2), yPos, text, size, style);
  426. }
  427.  
  428. /**
  429.  * Pre-render minimap
  430.  *
  431.  * Rendering the minimap each frame is far too expensive, so sacrifice about a second of gameplay
  432.  * to render it on the first run of the level guide. Also does some other misc setup such as
  433.  * allocating space for sprites and determining letterbox size.
  434.  */
  435. void precalc_init() {
  436.   if(precalc_done > 0) {
  437.     return;
  438.   }
  439.  
  440.   precalc_x = 0;
  441.   precalc_y = 0;
  442.   advertised = false;
  443.   letterbox = int(jjResolutionWidth / 12.5); //pretty arbitrary
  444.   letterbox_hue = (jjColorDepth == 8) ? 255 : letterbox_huehue;
  445.  
  446.   //allocate space to save the minimap etc as sprites
  447.   while (jjAnimSets[ANIM::CUSTOM[customAnimID]].firstAnim != 0) ++customAnimID;
  448.   array<uint> customFrames = {3};
  449.   jjAnimSets[ANIM::CUSTOM[customAnimID]].allocate(customFrames);
  450.    
  451.   //calculate drawable area for minimap - ignore empty space surrounding the level
  452.   minimap_width = jjResolutionWidth - (letterbox * 2);
  453.   minimap_height = jjResolutionHeight - (letterbox * 2) - 25;
  454.   for(int y = 0; y < jjLayers[4].height; y += 1) {
  455.     for(int x = 0; x < jjLayers[4].width; x += 1) {
  456.       if(jjLayers[4].tileGet(x, y) != 0) {
  457.         layer4_start_x = min(layer4_start_x, x);
  458.         layer4_start_y = min(layer4_start_y, y);
  459.         layer4_end_x = max(layer4_end_x, x);
  460.         layer4_end_y = max(layer4_end_y, y);
  461.       }
  462.     }
  463.   }
  464.  
  465.   //scale full level to minimap size
  466.   int width = layer4_end_x - layer4_start_x + 1;
  467.   int height = layer4_end_y - layer4_start_y + 1;
  468.   float aspect = float(width) / float(height);
  469.   float minimap_aspect = float(minimap_width) / float(minimap_height);
  470.   int half_width = int(minimap_width / 2);
  471.   if(aspect < minimap_aspect) {
  472.     minimap_y_offset = 0;
  473.     minimap_x_offset = int((minimap_width - (minimap_height * aspect)) / 2);
  474.   } else {
  475.     minimap_x_offset = 0;
  476.     minimap_y_offset = int((minimap_height - (minimap_height * (minimap_aspect / aspect))) / 2);
  477.   }
  478.  
  479.   minimap_x_step = float(width * 32) / float(minimap_width - (minimap_x_offset * 2.0));
  480.   minimap_y_step = float(height * 32) / float(minimap_height - (minimap_y_offset * 2.0));
  481.    
  482.   //render minimap
  483.   @minimap = jjPIXELMAP(uint(minimap_width - (minimap_x_offset * 2.0)), uint(minimap_height - (minimap_y_offset * 2.0)));
  484.   levelX = layer4_start_x * 32;
  485.   levelY = layer4_start_y * 32;
  486.  
  487.   precalc_done = 1;
  488. }
  489.  
  490. /**
  491.  * Render minimap
  492.  *
  493.  * Takes a number of pixels as an argument; only so many pixels will be rendered. This
  494.  * allows JJ2 to render the minimap in the background while gameplay continues, instead
  495.  * of the game hanging while rendering. Omitting the argument renders the full minimap
  496.  * (or what is left to be rendered)
  497.  */
  498. void precalc_pixels(int pixels = 0) {
  499.   if(precalc_active || precalc_done != 1) {
  500.     return;
  501.   }
  502.  
  503.   precalc_done = 1;
  504.   precalc_active = true;
  505.   int pixels_done = 0;
  506.   if(pixels == 0) {
  507.     pixels = minimap.height * minimap.width + 1;
  508.   }
  509.  
  510.   for(; precalc_y < int(minimap.height); precalc_y += 1) {
  511.     for(; precalc_x < int(minimap.width); precalc_x += 1) {
  512.       pixels_done += 1;
  513.       int tileX = int(levelX / 32);
  514.       int tileY = int(levelY / 32);
  515.       for(int l = 7; l > 0; l -= 1) {
  516.         if(jjLayers[l].xSpeed == 1 && jjLayers[l].ySpeed == 1) {
  517.           if((tileX > jjLayers[l].width && !jjLayers[l].tileWidth) || tileY > jjLayers[l].height && !jjLayers[l].tileHeight) {
  518.             continue;
  519.           }
  520.           int tileID = jjLayers[l].tileGet(tileX % jjLayers[l].width, tileY % jjLayers[l].height);
  521.           if(tileID > 0) {
  522.             jjPIXELMAP tile(tileID);
  523.             int color = tile[int(levelX % 32), int(levelY % 32)];
  524.             if(color > 0) {
  525.               minimap[precalc_x, precalc_y] = color;
  526.             }
  527.           }
  528.         }
  529.       }
  530.       levelX += minimap_x_step;
  531.       if(pixels_done >= pixels) {
  532.         precalc_x += 1;
  533.         precalc_active = false;
  534.         return;
  535.       }
  536.     }
  537.     levelX = layer4_start_x * 32;
  538.     levelY += minimap_y_step;
  539.     precalc_x = 0;
  540.   }
  541.  
  542.   precalc_done = 2;
  543.   finish_precalc();
  544.   precalc_active = false;  
  545. }
  546.  
  547. /**
  548.  * Finish precalc
  549.  *
  550.  * Renders item icons to the minimap and sets up some misc stuff
  551.  */
  552. void finish_precalc() {  
  553.   //show location of important items on the minimap
  554.   find_hotspots();
  555.  
  556.   //icons for each category and corresponding sprites
  557.   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
  558.                         ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, //powerups
  559.                         ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PICKUPS, ANIM::PLUS_COMMON}; //shields
  560.   array<int> anims =   {4, 8, 21, 2, 37, 28, 28, 52,
  561.                         60, 61, 62, 63, 64, 65, 66, 67,
  562.                         31, 10, 51, 2};
  563.   if(jjLocalPlayers[0].charCurr == CHAR::SPAZ) { anims[8] = 83; } //spaz blaster monitor
  564.   if(jjLocalPlayers[0].charCurr == CHAR::LORI) { sprites[8] = ANIM::PLUS_COMMON; anims[8] = 5; } //lori blaster monitor
  565.  
  566.   for(uint i = 0; i < hotspots.length(); i += 1) {
  567.     array<int> has_category = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
  568.    
  569.     //first determine where on the minimap the hotspot is and what kind of items it contains
  570.     int hotspot_x = 0;
  571.     int hotspot_y = 0;
  572.     for(uint j = 0; j < hotspots[i].things.length(); j += 1) {
  573.       interesting_thing t = hotspots[i].things[j];
  574.       hotspot_x += t.xPos;
  575.       hotspot_y += t.yPos;
  576.       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;
  577.       if(has_category[1] == 0) has_category[1] = ((t.eventID == OBJECT::CTFBASE || t.eventID == AREA::PATH) && t.team == 1) ? 1 : 0;
  578.       if(has_category[2] == 0) has_category[2] = (t.eventID == OBJECT::CARROT || t.eventID == OBJECT::FULLENERGY) ? 1 : 0;
  579.       if(has_category[4] == 0) has_category[4] = (t.eventID == OBJECT::SILVERCOIN || t.eventID == OBJECT::GOLDCOIN) ? 1 : 0;
  580.       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;
  581.       if(has_category[6] == 0) has_category[6] = (t.eventID == OBJECT::EOLPOST || t.eventID == AREA::EOL || t.eventID == AREA::WARPEOL) ? 1 : 0;
  582.       if(has_category[7] == 0) has_category[7] = (t.eventID == AREA::MPSTART || (t.eventID == AREA::PATH && t.team < 0)) ? 1 : 0;
  583.       for(int k = 8; k < 16; k += 1) {
  584.         if(has_category[k] == 0) has_category[k] = (t.eventID == power_ups[k - 8]) ? 1 : 0;
  585.       }
  586.       for(int k = 16; k < 20; k += 1) {
  587.         if(has_category[k] == 0) has_category[k] = (t.eventID == shields[k - 16]) ? 1 : 0;
  588.       }
  589.     }
  590.    
  591.     hotspot_x /= hotspots[i].things.length();
  592.     hotspot_y /= hotspots[i].things.length();
  593.     hotspot_x = int((hotspot_x - (layer4_start_x * 32)) / minimap_x_step);
  594.     hotspot_y = int((hotspot_y - (layer4_start_y * 32)) / minimap_y_step);
  595.     hotspots[i].levelX = hotspot_x;
  596.     hotspots[i].levelY = hotspot_y;
  597.    
  598.    
  599.     //then draw a small panel with icons for each type present
  600.     int panel_width = int(panel_scale * 0.75);
  601.     int panel_height = int(panel_scale * 1.5);
  602.    
  603.     panel_width += sum(has_category) * int(panel_scale * 1.25);
  604.     hotspots[i].width = panel_width;
  605.     hotspots[i].height = panel_height;
  606.    
  607.     //offsets - make sure the panel is within bounds
  608.     int xo = max(0, hotspot_x - (panel_width / 2));
  609.     int yo = max(0, hotspot_y - (panel_height / 2));
  610.     xo = min(xo, minimap_width - (minimap_x_offset * 2) - panel_width);
  611.     yo = min(yo, minimap_height - (minimap_y_offset * 2) - panel_height);
  612.    
  613.     //draw it
  614.     for(int x = 0; x < panel_width; x += 1) {
  615.       for(int y = 0; y < panel_height; y += 1) {
  616.         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)) {
  617.           continue; //rounded corners
  618.         }
  619.         int colour = panel_colour;
  620.         if(y <= 2 || y >= (panel_height - 3) || x <= 2 || x >= (panel_width - 3)) {
  621.           colour += 1;
  622.         }
  623.         if(y <= 1 || y >= (panel_height - 2) || x <= 1 || x >= (panel_width - 2)) {
  624.           colour += 1;
  625.         }
  626.         if(y <= 0 || y >= (panel_height - 1) || x <= 0 || x >= (panel_width - 1)) {
  627.           colour += 1;
  628.         }
  629.         minimap[x + xo, y + yo] = colour;
  630.       }
  631.     }
  632.    
  633.     //then draw the icons on the panel
  634.     for(uint j = 0; j < has_category.length(); j += 1) {
  635.       if(has_category[j] == 1) {
  636.         int sprite_offset = (panel_scale / 2);
  637.         for(uint k = 0; k < j; k += 1) {
  638.           if(has_category[k] == 1) {
  639.             sprite_offset += int(panel_scale * 1.25);
  640.           }
  641.         }
  642.        
  643.         jjPIXELMAP sprite(jjAnimFrames[jjAnimations[jjAnimSets[sprites[j]].firstAnim + anims[j]].firstFrame]);
  644.        
  645.         for(int x = 0; x < panel_scale; x += 1) {
  646.           for(int y = 0; y < panel_scale; y += 1) {
  647.             int ref_x = int(x * float(sprite.width) / float(panel_scale));
  648.             int ref_y = int(y * float(sprite.height) / float(panel_scale));
  649.             if(sprite[ref_x, ref_y] != 0) {
  650.               minimap[x + sprite_offset + xo, y + yo + int(panel_scale * 0.25)] = sprite[ref_x, ref_y];
  651.             }
  652.           }
  653.         }
  654.       }
  655.     }
  656.    
  657.     hotspots[i].originX = xo + letterbox + minimap_x_offset;
  658.     hotspots[i].originY = yo + letterbox + 25 + minimap_y_offset;
  659.   }
  660.  
  661.   jjANIMFRAME@ minimapframe = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::CUSTOM[customAnimID]].firstAnim].firstFrame];
  662.   minimap.save(minimapframe);
  663.  
  664.   precalc_wait = 10;
  665.   precalc_done = 3;
  666. }
  667.  
  668. /**
  669.  * Find hotspots
  670.  *
  671.  * Looks for objects that are of interest (powerups, health, shields, bases) and
  672.  * saves a list of locations; objects that are near to each other are clustered
  673.  */
  674. void find_hotspots() {
  675.   //see which objects are worth showing in the guide
  676.   array<interesting_thing> objects_found = {};
  677.   hotspots.removeRange(0, hotspots.length());
  678.  
  679.   for(int i = 1; i < jjObjectCount; i += 1) {
  680.     jjOBJ@ obj = jjObjects[i];
  681.     int e = obj.eventID;
  682.     if(e == OBJECT::GENERATOR) {
  683.       e = obj.var[3];
  684.     }
  685.     int team = -1;
  686.     if(e == OBJECT::CTFBASE) {
  687.       team = obj.var[1];
  688.     }
  689.     if(obj.isActive && is_interesting(e)) {
  690.       interesting_thing thing();
  691.       thing.xPos = int(obj.xOrg);
  692.       thing.yPos = int(obj.yOrg);
  693.       thing.eventID = e;
  694.       thing.objectID = i;
  695.       thing.team = team;
  696.       objects_found.insertLast(thing);
  697.     }
  698.   }
  699.  
  700.   //we may get duplicates of what we found in jjObjects here, but that doesn't matter
  701.   //since the minimap looks at types of objects, not the amount
  702.   for(int y = 0; y < jjLayers[4].height; y += 1) {
  703.     for(int x = 0; x < jjLayers[4].width; x += 1) {
  704.       int eventID = jjEventGet(x, y);
  705.       int difficulty = jjParameterGet(x, y, -4, 2);
  706.       if(eventID == OBJECT::GENERATOR) {
  707.         eventID = jjParameterGet(x, y, 0, 8);
  708.       }
  709.       int team = -1;
  710.       if(eventID == OBJECT::CTFBASE) {
  711.         team = jjParameterGet(x, y, 0, 1);
  712.       }
  713.       if(eventID == AREA::PATH) {
  714.         int speed = jjParameterGet(x, y, 0, 6);
  715.         if(speed == 0) {
  716.           team = 0;
  717.         } else if(speed == 21) {
  718.           team = 1;
  719.         }
  720.       }
  721.       if(eventID > 0 && is_interesting(eventID, difficulty)) {
  722.         interesting_thing thing();
  723.         thing.xPos = x * 32 + 16;
  724.         thing.yPos = y * 32 + 16;
  725.         thing.eventID = eventID;
  726.         thing.difficulty = difficulty;
  727.         thing.team = team;
  728.         objects_found.insertLast(thing);
  729.       }
  730.     }
  731.   }
  732.  
  733.   //check which of those are actually relevant to the current game mode
  734.   for(int i = objects_found.length() - 1; i >= 0; i -= 1) {
  735.     if(!is_relevant(objects_found[i])) {
  736.       objects_found.removeAt(i);
  737.     } else {
  738.       if(affected_by_gravity.find(objects_found[i].eventID) >= 0) {
  739.         objects_found[i].yPos += jjMaskedTopVLine(objects_found[i].xPos, objects_found[i].yPos, jjLayers[4].height * 32);
  740.       }
  741.       objects_found[i].typetext = get_typetext(objects_found[i]);
  742.       if(objects_found[i].eventID == OBJECT::CTFBASE) {
  743.         if(jjGameCustom == GAME::DOM) {
  744.           objects_found[i].width = objects_found[i].height = 64;
  745.         } else {
  746.           objects_found[i].width = 128;
  747.           objects_found[i].height = 96;
  748.         }
  749.       }
  750.     }
  751.   }
  752.  
  753.   //cluster nearby objects into hotspots
  754.   while(objects_found.length() > 0) {
  755.     cluster new_collection;
  756.     new_collection.things.insertLast(objects_found[0]);
  757.     objects_found.removeAt(0);
  758.     for(int i = objects_found.length() - 1; i >= 0; i -= 1) {
  759.       for(uint j = 0; j < new_collection.things.length(); j += 1) {
  760.         if(objects_are_near(new_collection.things[j], objects_found[i])) {
  761.           new_collection.things.insertLast(objects_found[i]);
  762.           objects_found.removeAt(i);
  763.           break;
  764.         }
  765.       }
  766.     }
  767.     hotspots.insertLast(new_collection);
  768.   }
  769.  
  770.   max_page = hotspots.length();
  771. }
  772.  
  773. /**
  774.  * Check if event is interesting
  775.  *
  776.  * "Interesting" here means that it may potentially be shown; whether it will actually
  777.  * be shown to players depends on other factors
  778.  */
  779. bool is_interesting(int e, int difficulty = 0) {
  780.   if((difficulty == 1 || difficulty == 2) && jjGameCustom == GAME::DOM) {
  781.     return true;
  782.   }
  783.   return interesting_events.find(e) >= 0;
  784. }
  785.  
  786. /**
  787.  * Check if thing is *relevant*
  788.  *
  789.  * Relevant means it's useful to point out in the current game mode; thus relevant
  790.  * things are a subset of interesting things
  791.  */
  792. bool is_relevant(interesting_thing t) {
  793.   int e = t.eventID;
  794.   if(e == AREA::WARP) {
  795.     return (jjGameCustom == GAME::FR && t.difficulty == 1);
  796.   }
  797.   if(e == OBJECT::EOLPOST || e == AREA::EOL || e == AREA::WARPEOL) {
  798.     return (jjGameCustom == GAME::NOCUSTOM && (jjGameMode == GAME::COOP || jjGameMode == GAME::TREASURE));
  799.   }
  800.   if(e == OBJECT::SILVERCOIN || e == OBJECT::GOLDCOIN) {
  801.     return (mode_mutator == "br"); //detect bank robbery here, somehow
  802.   }
  803.   if(e == AREA::PATH) {
  804.     return (mode_mutator == "ons");
  805.   }
  806.   if(e == OBJECT::ICEAMMO3 || e == OBJECT::ICEAMMO15) {
  807.     return (jjGameCustom == GAME::JB || mode_mutator == "ons");
  808.   }
  809.   if(e == AREA::MPSTART) {
  810.     return (jjGameCustom == GAME::JB);
  811.   }
  812.   if(e == AREA::TEXT) {
  813.     return (jjGameCustom == GAME::DOM && (t.difficulty == 1 || t.difficulty == 2));
  814.   }
  815.   if(e == OBJECT::CTFBASE) {
  816.     return (jjGameCustom == GAME::NOCUSTOM && jjGameMode == GAME::CTF) || (jjGameCustom == GAME::DOM || jjGameCustom == GAME::FR || jjGameCustom == GAME::DCTF || jjGameCustom == GAME::HEAD);
  817.   }
  818.   if(jjGameCustom == GAME::DOM && (t.difficulty == 1 || t.difficulty == 2)) {
  819.     return true;
  820.   }
  821.   return interesting_events.find(e) >= 0;
  822. }
  823.  
  824. /**
  825.  * Get text describing type of thing
  826.  */
  827. string get_typetext(interesting_thing t) {
  828.   int e = t.eventID;
  829.   if(e == AREA::WARP) {
  830.     return "Flag";
  831.   }
  832.   if(e == OBJECT::EOLPOST || e == AREA::EOL || e == AREA::WARPEOL) {
  833.     return "Exit";
  834.   }
  835.   if(e == OBJECT::SILVERCOIN || e == OBJECT::GOLDCOIN) {
  836.     return "Coins";
  837.   }
  838.   if(e == OBJECT::ICEAMMO3 || e == OBJECT::ICEAMMO15) {
  839.     return (mode_mutator == "ons") ? "Node builder ammo" : "Freezer ammo";
  840.   }
  841.   if(e == AREA::MPSTART) {
  842.     return "Jail";
  843.   }
  844.   if(e == OBJECT::CTFBASE) {
  845.     return (jjGameCustom == GAME::DOM) ? "Control point" : "Base";
  846.   }
  847.   if(e == AREA::PATH) {
  848.     return "Control point";
  849.   }
  850.   if(power_ups.find(e) >= 0) {
  851.     return "PowerUp";
  852.   }
  853.   if(e == OBJECT::CARROT || e == OBJECT::FULLENERGY) {
  854.     return "Health";
  855.   }
  856.   if(jjGameCustom == GAME::DOM && (t.difficulty == 1 || t.difficulty == 2)) {
  857.     return "Control point";
  858.   }
  859.   if(e == OBJECT::FIRESHIELD || e == OBJECT::WATERSHIELD || e == OBJECT::PLASMASHIELD || e == OBJECT::LASERSHIELD) {
  860.     return "Shield";
  861.   }
  862.   return "MYSTERY OBJECT";
  863. }
  864.  
  865. /**
  866.  * Check if two objects are near each other
  867.  *
  868.  * Radius within which object must be is half the screen; vary if objects don't cluster easily enough
  869.  */
  870. bool objects_are_near(interesting_thing one, interesting_thing two) {
  871.   float xRadius = jjResolutionWidth / 4;
  872.   float yRadius = jjResolutionHeight / 4;
  873.   float xMin = one.xPos - xRadius;
  874.   float xMax = one.xPos + xRadius;
  875.   float yMin = one.yPos - yRadius;
  876.   float yMax = one.yPos + yRadius;
  877.   return (two.xPos > xMin && two.xPos < xMax && two.yPos > yMin && two.yPos < yMax);
  878. }
  879.  
  880. /**
  881.  * Minimal public interface support just so other mutators can detect this one's presence
  882.  */
  883. shared interface PublicInterface : jjPUBLICINTERFACE { }
  884. class PublicClass : PublicInterface { string getVersion() const { return "1.1"; } }
  885. PublicClass publicInstance;
  886. PublicClass@ onGetPublicInterface() { return publicInstance; }