Name | Author | Game Mode | Rating | |||||
---|---|---|---|---|---|---|---|---|
Minimap | minmay | Mutator | 10 |
// by may
// 4 oct 2023
// CC0 / public domain
#pragma name "Minimap"
// This mutator has no effect if MayLib (and its fancier minimap) is loaded.
bool enable = true;
const bool SHOW_ALLIES = true;
const bool SHOW_ENEMIES = false;
const int MAX_PLAYERS = 32;
// replaced with the first character in minimapKey.cfg if it exists and isn't empty
uint8 minimapToggleKey = 0x4D; // M
bool gameModeHasTeams() {
return jjGameMode == GAME::CTF || jjGameCustom == GAME::DCTF ||
jjGameCustom == GAME::DOM || jjGameCustom == GAME::FR ||
jjGameCustom == GAME::JB || jjGameCustom == GAME::TB || jjGameCustom == GAME::TLRS;
}
int nextCustomAnimSet() {
for (int i = 0; i < 256; i++) {
if (jjAnimSets[ANIM::CUSTOM[i]].firstAnim == 0) {
return ANIM::CUSTOM[i];
}
}
return -1;
}
array<int> keyDownTick(256,-1000);
array<int> keyUpTick(256,-1000);
int lastKeyUpdateTick = -1000;
// Returns true if a key was just pressed, i.e. it's down now but wasn't
// down last frame.
bool keyPressed(uint key) {
return keyDownTick[key & 0xFF] == lastKeyUpdateTick && keyUpTick[key & 0xFF] == lastKeyUpdateTick-1;
}
class MayMinimap {
uint width = 0;
uint height = 0;
uint8 emptyColor = 39; // very dark blue
uint8 wallColor = 36; // less dark blue
// start indices of 8-color gradients for showing players on the minimap
uint8 selfColor = 16; // green
uint8 allyColor = 64; // beige
uint8 enemyColor = 24; // red
uint animFrameStart;
uint yOffset = 30;
uint scale = 1;
MayMinimap(uint startAnimFrameToReplace) {
animFrameStart = startAnimFrameToReplace;
jjLAYER@ layer = jjLayers[4];
int newWidth = layer.width;
int newHeight = layer.height;
if (newWidth <= 0 || newHeight <= 0) {
width = 0;
height = 0;
return;
}
width = newWidth;
height = newHeight;
jjPIXELMAP image(width, height);
jjPIXELMAP bigImage(width*2, height*2);
for (int y = 0; y < layer.height; y++) {
for (int x = 0; x < layer.width; x++) {
bool quadrant1 = layer.maskedPixel(x*32+8,y*32+8);
bool quadrant2 = layer.maskedPixel(x*32+24,y*32+8);
bool quadrant3 = layer.maskedPixel(x*32+8,y*32+24);
bool quadrant4 = layer.maskedPixel(x*32+24,y*32+24);
uint px = x;
uint py = y;
uint8 curEmptyColor = emptyColor;
bigImage[px*2, py*2] = quadrant1 ? wallColor : emptyColor;
bigImage[px*2+1, py*2] = quadrant2 ? wallColor : emptyColor;
bigImage[px*2, py*2+1] = quadrant3 ? wallColor : emptyColor;
bigImage[px*2+1, py*2+1] = quadrant4 ? wallColor : emptyColor;
// XXX: this is overly pessimistic, e.g. a 32-pixel wide tunnel
// centered on the edge between tiles will get rendered as a solid wall.
image[x, y] = (quadrant1 || quadrant2 || quadrant3 || quadrant4) ? wallColor : emptyColor;
}
}
image.save(jjAnimFrames[startAnimFrameToReplace]);
bigImage.save(jjAnimFrames[startAnimFrameToReplace+1]);
jjAnimFrames[startAnimFrameToReplace].hotSpotX = 0;
jjAnimFrames[startAnimFrameToReplace].hotSpotY = 0;
jjAnimFrames[startAnimFrameToReplace+1].hotSpotX = 0;
jjAnimFrames[startAnimFrameToReplace+1].hotSpotY = 0;
}
int _drawX(float xPos) {
return jjResolutionWidth-width*scale+(int(xPos*scale) >> 5);
}
int _drawY(float yPos) {
return (int(yPos*scale) >> 5) + yOffset;
}
void _drawPowerup(jjCANVAS@ canvas, int x, int y, int eventID) {
uint8 color = 0;
switch (eventID) {
case OBJECT::BLASTERPOWERUP:
color = 73; // grey
break;
case OBJECT::BOUNCERPOWERUP:
color = 90; // purple
break;
case OBJECT::ICEPOWERUP:
color = 35; // blue
break;
case OBJECT::SEEKERPOWERUP:
color = 41; // yellow-orange
break;
case OBJECT::RFPOWERUP:
color = 25; // red
break;
case OBJECT::TOASTERPOWERUP:
// powered up toaster ammo is purple and yellow,
// and its bullets are blue, so obviously we
// indicate its powerup with pink
color = 49;
break;
case OBJECT::TNTPOWERUP:
color = 65; // like beige who cares
break;
case OBJECT::GUN8POWERUP:
color = 32; // light blue
break;
case OBJECT::GUN9POWERUP:
color = 44; // dark orange
break;
default:
return;
}
// powerups are drawn as pyramids
canvas.drawPixel(x, y-1, color);
canvas.drawPixel(x-1, y, color);
canvas.drawPixel(x, y, color);
canvas.drawPixel(x+1, y, color);
}
void draw(jjCANVAS@ canvas) {
if (scale > 0) {
canvas.drawSpriteFromCurFrame(jjResolutionWidth-width*scale, yOffset, animFrameStart+scale-1, 0, SPRITE::TRANSLUCENT);
// If splitscreeners are on different teams, and showAllies is on but showAllPlayers
// is off, allies won't be drawn, since it would mean one or more splitscreeners
// could see the position of their enemies!
bool drawAllies = gameModeHasTeams() && SHOW_ALLIES;
if (drawAllies && !SHOW_ENEMIES) {
int splitscreenerTeam = jjLocalPlayers[0].team;
for (int i = 1; i < jjLocalPlayerCount; i++) {
if (jjLocalPlayers[i].team != splitscreenerTeam) drawAllies = false;
}
}
for (uint i = 0; i < MAX_PLAYERS; i++) {
if (jjPlayers[i].isInGame && jjPlayers[i].health > 0) {
uint8 color;
if (jjPlayers[i].isLocal) {
color = selfColor+2;
} else if (drawAllies && (jjPlayers[i].team == jjLocalPlayers[0].team)) {
color = allyColor+2;
} else if (SHOW_ENEMIES) {
color = enemyColor;
} else { // don't draw this player
continue;
}
int x = _drawX(jjPlayers[i].xPos);
int y = _drawY(jjPlayers[i].yPos);
canvas.drawPixel(x, y, color);
// Flag holders and Eva's Ring holders twinkle and show health
if (jjPlayers[i].flag != 0 || jjPlayers[i] is jjTokenOwner) {
if ((jjGameTicks >> 3) & 1 == 1) {
canvas.drawPixel(x-1, y, color);
canvas.drawPixel(x+1, y, color);
canvas.drawPixel(x, y-1, color);
canvas.drawPixel(x, y+1, color);
}
int health = jjPlayers[i].health;
for (int i = 0; i < health; i++) {
canvas.drawPixel(x-health+i*2+1, y-3, 49);
}
}
}
}
for (uint i = 1; i < jjObjectCount; i++) {
jjOBJ @obj = jjObjects[i];
if (obj.isActive) {
// draw CTF bases as Xes of the team's color
if (obj.eventID == OBJECT::CTFBASE) {
int x = _drawX(obj.xOrg);
int y = _drawY(obj.yOrg);
uint8 color;
switch (obj.var[1]) {
case 0:
color = 34; // blue base
break;
case 1:
color = 25; // red base
break;
case 2:
color = 18; // green base
break;
case 3:
default:
color = 40; // yellow base
break;
}
canvas.drawPixel(x-1, y-1, color);
canvas.drawPixel(x+1, y-1, color);
canvas.drawPixel(x, y, color);
canvas.drawPixel(x-1, y+1, color);
canvas.drawPixel(x+1, y+1, color);
} else if (obj.eventID == OBJECT::GENERATOR) {
int eventIDToGenerate = obj.var[3];
int x = _drawX(obj.xPos);
int y = _drawY(obj.yPos);
switch (eventIDToGenerate) {
// draw carrot generators as diagonal orange lines
case OBJECT::CARROT:
case OBJECT::FULLENERGY:
canvas.drawPixel(x, y, 42);
canvas.drawPixel(x-1, y+1, 42);
break;
case OBJECT::BLASTERPOWERUP:
case OBJECT::BOUNCERPOWERUP:
case OBJECT::ICEPOWERUP:
case OBJECT::SEEKERPOWERUP:
case OBJECT::RFPOWERUP:
case OBJECT::TOASTERPOWERUP:
case OBJECT::TNTPOWERUP: // yeah sure let's pretend this gets used
case OBJECT::GUN8POWERUP:
case OBJECT::GUN9POWERUP:
_drawPowerup(canvas, x, y, eventIDToGenerate);
break;
default:
break;
}
}
}
}
}
}
void processToggleCommand() {
scale = (scale+1)%3;
}
}
MayMinimap @minimap;
void onLevelLoad() {
jjPUBLICINTERFACE@ maylib = jjGetPublicInterface("MayLib.asc");
if (maylib !is null) {
enable = false;
return;
}
int customAnimSet = nextCustomAnimSet();
array<uint> animAllocs = {2};
jjAnimSets[customAnimSet].allocate(animAllocs);
@minimap = @MayMinimap(jjAnimations[jjAnimSets[customAnimSet].firstAnim].firstFrame);
jjSTREAM keyfile("minimapKey.cfg");
if (!keyfile.isEmpty()) keyfile.pop(minimapToggleKey);
// assume ASCII lowercase letters are meant to be the letter and not the
// key code. This does mean you can't use most numpad or function keys
// (since their keycodes overlap with lowercase letter ASCII character codes)
// but whatever, it's not like you need to hit the minimap key often.
if (minimapToggleKey >= 97 && minimapToggleKey <= 122) minimapToggleKey -= 32;
}
void onMain() {
if (!enable) return;
for (uint key = 0; key < 256; key++) {
if (jjKey[key]) {
keyDownTick[key] = jjGameTicks;
} else {
keyUpTick[key] = jjGameTicks;
}
}
lastKeyUpdateTick = jjGameTicks;
}
// There's not currently a way to check if people are chatting, but this
// function is at least not called if the player is on keyboard and chatting.
// Controller players will just have to deal with unwanted minimap toggles while
// chatting...
void onPlayerInput(jjPLAYER@ player) {
if (!enable) return;
if (player.localPlayerID == 0 && keyPressed(minimapToggleKey)) {
minimap.processToggleCommand();
}
}
bool onDrawGameModeHUD(jjPLAYER@ player, jjCANVAS@ canvas) {
if (!enable) return false;
if (player.localPlayerID == 0) {
minimap.draw(canvas);
}
return false;
}
Jazz2Online © 1999-INFINITY (Site Credits). We have a Privacy Policy. Jazz Jackrabbit, Jazz Jackrabbit 2, Jazz Jackrabbit Advance and all related trademarks and media are ™ and © Epic Games. Lori Jackrabbit is © Dean Dodrill. J2O development powered by Loops of Fury and Chemical Beats.
Eat your lima beans, Johnny.