Downloads containing minimap.mut

Downloads
Name Author Game Mode Rating
JJ2+ Only: MinimapFeatured Download minmay Mutator 10 Download file

File preview

// 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;
}