Minimap with destinations

This commit is contained in:
dazedanon 2025-12-21 11:59:06 -06:00
parent 4477ef7fbe
commit c47f823c83

View file

@ -3,7 +3,7 @@
//=============================================================================
/*:
* @plugindesc Adds a minimap/zoom-out feature. Press M to toggle a zoomed-out view of the entire map.
* @plugindesc Adds a minimap/zoom-out feature with exit labels, zoom and pan controls.
* @author Dazed Translations
*
* @param Toggle Key
@ -14,7 +14,17 @@
* Press the M key while on the map to toggle a zoomed-out minimap view.
* Shows the entire map scaled to fit the screen.
* Character sprites are hidden, and a marker shows your position.
* Press M again to return to normal view.
*
* Controls:
* - M: Close minimap
* - Mouse Wheel / Shift / Ctrl: Zoom in/out
* - Arrow Keys / WASD / Mouse Drag: Pan the map
* - R: Reset zoom and pan
*
* Features:
* - Exit locations are numbered and show destination names in a legend
* - Nearby exits to the same location are grouped together
* - Zoom and pan for easier viewing of large maps
*/
(function() {
@ -28,6 +38,25 @@
// Track minimap state
let minimapActive = false;
// Zoom and pan state
let minimapZoom = 1.0;
let minimapPanX = 0;
let minimapPanY = 0;
const MIN_ZOOM = 0.5;
const MAX_ZOOM = 3.0;
const ZOOM_STEP = 0.1;
const PAN_SPEED = 15;
// Mouse drag state
let isDragging = false;
let dragStartX = 0;
let dragStartY = 0;
let dragStartPanX = 0;
let dragStartPanY = 0;
// Exit location data
let exitLocations = [];
// Export minimap state for other plugins/systems to check
window.isMinimapActive = function() {
return minimapActive;
@ -43,10 +72,17 @@
};
//=========================================================================
// Input - Add minimap key
// Input - Add minimap key and zoom/pan keys
//=========================================================================
Input.keyMapper[toggleKeyCode] = 'minimap';
Input.keyMapper[82] = 'minimapReset'; // R key
Input.keyMapper[16] = 'minimapZoomIn'; // Shift key
Input.keyMapper[17] = 'minimapZoomOut'; // Ctrl key
Input.keyMapper[87] = 'minimapUp'; // W key
Input.keyMapper[65] = 'minimapLeft'; // A key
Input.keyMapper[83] = 'minimapDown'; // S key
Input.keyMapper[68] = 'minimapRight'; // D key
//=========================================================================
// Auto-close minimap when any event starts
@ -91,6 +127,9 @@
// Update player marker position and atmospheric effects if minimap is active
if (minimapActive) {
// Handle zoom and pan controls
this.updateMinimapZoomPan();
if (this._minimapPlayerMarker) {
this.updateMinimapPlayerMarker();
}
@ -98,6 +137,237 @@
}
};
Scene_Map.prototype.collectExitLocations = function() {
exitLocations = [];
const rawExits = [];
// Collect all transfer events
for (const event of $gameMap.events()) {
if (!event) continue;
const eventData = event.event();
if (!eventData) continue;
// Check all pages for Transfer Player command
for (const page of eventData.pages) {
if (!page || !page.list) continue;
for (const cmd of page.list) {
if (cmd.code === 201) { // Transfer Player command
const params = cmd.parameters;
if (params[0] === 0) { // Direct designation (not variable)
const targetMapId = params[1];
const mapName = this.getMapName(targetMapId);
rawExits.push({
x: event.x,
y: event.y,
mapId: targetMapId,
mapName: mapName
});
}
break; // Only need first transfer per page
}
}
}
}
// Group nearby exits going to the same destination
const GROUPING_DISTANCE = 3; // Tiles within this distance get grouped
const grouped = [];
const used = new Set();
for (let i = 0; i < rawExits.length; i++) {
if (used.has(i)) continue;
const exit = rawExits[i];
const group = {
positions: [{x: exit.x, y: exit.y}],
mapId: exit.mapId,
mapName: exit.mapName
};
used.add(i);
// Find nearby exits to same destination
for (let j = i + 1; j < rawExits.length; j++) {
if (used.has(j)) continue;
const other = rawExits[j];
if (other.mapId === exit.mapId) {
// Check if any position in group is close enough
let isNearby = false;
for (const pos of group.positions) {
const dx = Math.abs(other.x - pos.x);
const dy = Math.abs(other.y - pos.y);
if (dx <= GROUPING_DISTANCE && dy <= GROUPING_DISTANCE) {
isNearby = true;
break;
}
}
if (isNearby) {
group.positions.push({x: other.x, y: other.y});
used.add(j);
}
}
}
// Calculate center position for the group
let sumX = 0, sumY = 0;
for (const pos of group.positions) {
sumX += pos.x;
sumY += pos.y;
}
group.centerX = sumX / group.positions.length;
group.centerY = sumY / group.positions.length;
grouped.push(group);
}
// Assign numbers to grouped exits
let exitNumber = 1;
for (const group of grouped) {
group.number = exitNumber++;
exitLocations.push(group);
}
};
Scene_Map.prototype.getMapName = function(mapId) {
// Try to get map name from $dataMapInfos
if ($dataMapInfos && $dataMapInfos[mapId]) {
return $dataMapInfos[mapId].name || 'Unknown';
}
return 'Map ' + mapId;
};
Scene_Map.prototype.updateMinimapZoomPan = function() {
let zoomChanged = false;
let panChanged = false;
// Zoom controls
if (Input.isTriggered('minimapZoomIn') || Input.isRepeated('minimapZoomIn')) {
minimapZoom = Math.min(MAX_ZOOM, minimapZoom + ZOOM_STEP);
zoomChanged = true;
}
if (Input.isTriggered('minimapZoomOut') || Input.isRepeated('minimapZoomOut')) {
minimapZoom = Math.max(MIN_ZOOM, minimapZoom - ZOOM_STEP);
zoomChanged = true;
}
// Pan controls (arrow keys and WASD)
if (Input.isPressed('up') || Input.isPressed('minimapUp')) {
minimapPanY += PAN_SPEED;
panChanged = true;
}
if (Input.isPressed('down') || Input.isPressed('minimapDown')) {
minimapPanY -= PAN_SPEED;
panChanged = true;
}
if (Input.isPressed('left') || Input.isPressed('minimapLeft')) {
minimapPanX += PAN_SPEED;
panChanged = true;
}
if (Input.isPressed('right') || Input.isPressed('minimapRight')) {
minimapPanX -= PAN_SPEED;
panChanged = true;
}
// Reset controls
if (Input.isTriggered('minimapReset')) {
minimapZoom = 1.0;
minimapPanX = 0;
minimapPanY = 0;
zoomChanged = true;
panChanged = true;
SoundManager.playCursor();
}
// Mouse wheel zoom
if (this._minimapContent) {
const wheelY = TouchInput.wheelY;
if (wheelY !== 0) {
if (wheelY < 0) {
minimapZoom = Math.min(MAX_ZOOM, minimapZoom + ZOOM_STEP);
} else {
minimapZoom = Math.max(MIN_ZOOM, minimapZoom - ZOOM_STEP);
}
zoomChanged = true;
}
}
// Mouse drag pan
if (TouchInput.isPressed()) {
if (!isDragging) {
isDragging = true;
dragStartX = TouchInput.x;
dragStartY = TouchInput.y;
dragStartPanX = minimapPanX;
dragStartPanY = minimapPanY;
} else {
minimapPanX = dragStartPanX + (TouchInput.x - dragStartX);
minimapPanY = dragStartPanY + (TouchInput.y - dragStartY);
panChanged = true;
}
} else {
isDragging = false;
}
// Apply zoom and pan to content
if ((zoomChanged || panChanged) && this._minimapContent) {
this.applyMinimapTransform();
}
};
Scene_Map.prototype.applyMinimapTransform = function() {
if (!this._minimapContent) return;
// Calculate the center point for zooming
const centerX = Graphics.width / 2;
const centerY = Graphics.height / 2;
// Apply scale
this._minimapContent.scale.x = this._minimapScale * minimapZoom;
this._minimapContent.scale.y = this._minimapScale * minimapZoom;
// Calculate new offset to keep centered
const mapWidth = $gameMap.width() * this._minimapTileWidth;
const mapHeight = $gameMap.height() * this._minimapTileHeight;
const scaledWidth = mapWidth * this._minimapScale * minimapZoom;
const scaledHeight = mapHeight * this._minimapScale * minimapZoom;
const baseOffsetX = (Graphics.width - scaledWidth) / 2;
const baseOffsetY = (Graphics.height - scaledHeight) / 2;
this._minimapContent.x = baseOffsetX + minimapPanX;
this._minimapContent.y = baseOffsetY + minimapPanY;
// Update border to match content
if (this._minimapBorder) {
this._minimapBorder.scale.x = this._minimapScale * minimapZoom;
this._minimapBorder.scale.y = this._minimapScale * minimapZoom;
this._minimapBorder.x = baseOffsetX + minimapPanX - this._borderPadding * this._minimapScale * minimapZoom;
this._minimapBorder.y = baseOffsetY + minimapPanY - this._borderPadding * this._minimapScale * minimapZoom;
}
// Update exit destinations panel to stay next to map
if (this._exitDestinationsPanel) {
this._exitDestinationsPanel.scale.x = minimapZoom;
this._exitDestinationsPanel.scale.y = minimapZoom;
this._exitDestinationsPanel.x = baseOffsetX + minimapPanX + scaledWidth + this._exitPanelOffset * minimapZoom;
this._exitDestinationsPanel.y = baseOffsetY + minimapPanY;
}
// Store current offsets for marker positioning
this._currentOffsetX = this._minimapContent.x;
this._currentOffsetY = this._minimapContent.y;
this._currentScale = this._minimapScale * minimapZoom;
// Update exit markers positions
if (this._exitMarkers) {
this.updateExitMarkerPositions();
}
};
Scene_Map.prototype.updateMinimapAtmosphere = function() {
// Subtle flicker effect on the map content to simulate candlelight
if (this._minimapContent) {
@ -122,6 +392,15 @@
Scene_Map.prototype.showMinimap = function() {
if (!this._spriteset) return;
// Reset zoom and pan when opening
minimapZoom = 1.0;
minimapPanX = 0;
minimapPanY = 0;
isDragging = false;
// Collect exit location data
this.collectExitLocations();
// Hide the normal spriteset
this._spriteset.visible = false;
@ -191,8 +470,8 @@
// Render the tilemap to a bitmap
this.renderMinimapTiles();
// Draw ornate border around map
this.drawOrnateBorder(offsetX, offsetY, scaledWidth, scaledHeight);
// Create border as separate sprite that moves with content
this.createMinimapBorder(mapPixelWidth, mapPixelHeight);
// Add title text with gothic styling
this._minimapOverlay.bitmap.fontFace = 'GameFont';
@ -211,16 +490,29 @@
// Smaller instruction text
this._minimapOverlay.bitmap.fontSize = 14;
this._minimapOverlay.bitmap.textColor = '#8b7355';
this._minimapOverlay.bitmap.drawText('Press M to close', 0, Graphics.height - 18, Graphics.width, 20, 'center');
this._minimapOverlay.bitmap.drawText('M: Close | Arrows/WASD: Pan | Shift/Ctrl/Scroll: Zoom | R: Reset', 0, Graphics.height - 18, Graphics.width, 20, 'center');
// Draw legend
this.drawMinimapLegend();
this.addChild(this._minimapOverlay);
this.addChild(this._minimapBorder);
this.addChild(this._minimapContent);
// Create exit destinations panel (moves with map)
this.createExitDestinationsPanel(mapPixelWidth, mapPixelHeight);
// Create exit markers (as separate sprites for positioning with zoom/pan)
this.createExitMarkers();
// Create player marker
this.createMinimapPlayerMarker();
// Initialize current transform values and position all elements correctly
this._currentOffsetX = offsetX;
this._currentOffsetY = offsetY;
this._currentScale = scale;
this.applyMinimapTransform();
};
Scene_Map.prototype.drawAtmosphericBackground = function() {
@ -252,9 +544,21 @@
ctx.fillRect(0, 0, w, h);
};
Scene_Map.prototype.drawOrnateBorder = function(x, y, width, height) {
const ctx = this._minimapOverlay.bitmap.context;
Scene_Map.prototype.createMinimapBorder = function(mapPixelWidth, mapPixelHeight) {
const borderSize = 6;
const padding = 20; // Extra padding for the border sprite
// Create border sprite sized to fit around the map
this._minimapBorder = new Sprite();
const borderWidth = mapPixelWidth + padding * 2;
const borderHeight = mapPixelHeight + padding * 2;
this._minimapBorder.bitmap = new Bitmap(borderWidth, borderHeight);
const ctx = this._minimapBorder.bitmap.context;
const x = padding;
const y = padding;
const width = mapPixelWidth;
const height = mapPixelHeight;
// Outer dark border
ctx.strokeStyle = '#0d0805';
@ -310,6 +614,20 @@
ctx.beginPath();
ctx.arc(x + width, y + height, innerDot/2, 0, Math.PI * 2);
ctx.fill();
// Store the padding offset for positioning
this._borderPadding = padding;
// Position and scale to match content
this._minimapBorder.x = this._minimapOffsetX - padding * this._minimapScale;
this._minimapBorder.y = this._minimapOffsetY - padding * this._minimapScale;
this._minimapBorder.scale.x = this._minimapScale;
this._minimapBorder.scale.y = this._minimapScale;
};
// Keep old function for compatibility but it does nothing now
Scene_Map.prototype.drawOrnateBorder = function(x, y, width, height) {
// Border is now drawn as a separate sprite in createMinimapBorder
};
Scene_Map.prototype.drawMinimapLegend = function() {
@ -377,6 +695,159 @@
bitmap.drawText('Passage', legendX + dotSize + 10, legendY + lineHeight * 4 + 5, 150, 18, 'left');
};
Scene_Map.prototype.createExitDestinationsPanel = function(mapPixelWidth, mapPixelHeight) {
if (exitLocations.length === 0) return;
const lineHeight = 20;
const padding = 15;
const panelWidth = 280;
const panelHeight = 35 + exitLocations.length * lineHeight + padding;
// Create panel sprite
this._exitDestinationsPanel = new Sprite();
this._exitDestinationsPanel.bitmap = new Bitmap(panelWidth, panelHeight);
const bitmap = this._exitDestinationsPanel.bitmap;
const ctx = bitmap.context;
// Draw background panel
ctx.fillStyle = 'rgba(26, 20, 16, 0.9)';
ctx.fillRect(0, 0, panelWidth, panelHeight);
ctx.strokeStyle = '#8b7355';
ctx.lineWidth = 2;
ctx.strokeRect(0, 0, panelWidth, panelHeight);
// Inner border
ctx.strokeStyle = '#c9a959';
ctx.lineWidth = 1;
ctx.strokeRect(3, 3, panelWidth - 6, panelHeight - 6);
bitmap.fontFace = 'GameFont';
bitmap.fontSize = 14;
bitmap.textColor = '#d4af37';
bitmap.outlineColor = '#1a0a00';
bitmap.outlineWidth = 3;
// Title
const titleY = 8;
bitmap.drawText('~ Destinations ~', 0, titleY, panelWidth, 18, 'center');
// Underline
ctx.strokeStyle = '#8b7355';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padding, titleY + 22);
ctx.lineTo(panelWidth - padding, titleY + 22);
ctx.stroke();
bitmap.fontSize = 12;
bitmap.textColor = '#c9a959';
// Draw each exit destination (no limit now)
for (let i = 0; i < exitLocations.length; i++) {
const exit = exitLocations[i];
const y = 32 + i * lineHeight;
// Draw number circle
const circleX = padding + 10;
const circleY = y + 9;
ctx.fillStyle = '#7cfc00';
ctx.shadowColor = '#7cfc00';
ctx.shadowBlur = 3;
ctx.beginPath();
ctx.arc(circleX, circleY, 8, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#2a4a00';
ctx.lineWidth = 1;
ctx.stroke();
// Draw number
ctx.fillStyle = '#1a0a00';
ctx.font = 'bold 10px GameFont';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(exit.number.toString(), circleX, circleY);
ctx.textAlign = 'left';
// Draw destination name (full name, no truncation)
bitmap.drawText(exit.mapName, padding + 25, y, panelWidth - padding - 30, 18, 'left');
}
// Position panel to the right of the map
this._exitPanelOffset = 20; // Gap between map and panel
this._exitDestinationsPanel.x = this._minimapOffsetX + mapPixelWidth * this._minimapScale + this._exitPanelOffset;
this._exitDestinationsPanel.y = this._minimapOffsetY;
this.addChild(this._exitDestinationsPanel);
};
// Keep old function for compatibility but redirect to new one
Scene_Map.prototype.drawExitLegend = function() {
// Now handled by createExitDestinationsPanel
};
Scene_Map.prototype.createExitMarkers = function() {
this._exitMarkers = [];
for (const exit of exitLocations) {
const marker = new Sprite();
marker.bitmap = new Bitmap(24, 24);
const ctx = marker.bitmap.context;
const cx = 12, cy = 12;
// Draw green circle
ctx.fillStyle = '#7cfc00';
ctx.shadowColor = '#7cfc00';
ctx.shadowBlur = 4;
ctx.beginPath();
ctx.arc(cx, cy, 10, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#2a4a00';
ctx.lineWidth = 2;
ctx.stroke();
// Draw number
ctx.fillStyle = '#1a0a00';
ctx.font = 'bold 12px GameFont';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(exit.number.toString(), cx, cy);
marker.anchor.x = 0.5;
marker.anchor.y = 0.5;
// Store exit data on marker for positioning
marker._exitData = exit;
this._exitMarkers.push(marker);
this.addChild(marker);
}
this.updateExitMarkerPositions();
};
Scene_Map.prototype.updateExitMarkerPositions = function() {
if (!this._exitMarkers) return;
const offsetX = this._currentOffsetX || this._minimapOffsetX;
const offsetY = this._currentOffsetY || this._minimapOffsetY;
const scale = this._currentScale || this._minimapScale;
for (const marker of this._exitMarkers) {
const exit = marker._exitData;
const markerX = offsetX + (exit.centerX + 0.5) * this._minimapTileWidth * scale;
const markerY = offsetY + (exit.centerY + 0.5) * this._minimapTileHeight * scale;
marker.x = markerX;
marker.y = markerY;
marker.scale.x = Math.max(0.5, minimapZoom * 0.8);
marker.scale.y = Math.max(0.5, minimapZoom * 0.8);
}
};
Scene_Map.prototype.renderMinimapTiles = function() {
const mapWidth = $gameMap.width();
const mapHeight = $gameMap.height();
@ -520,7 +991,7 @@
}
}
// Draw important events as markers
// Draw important events as markers (except transfers - those are drawn as numbered markers separately)
for (const event of $gameMap.events()) {
if (!event) continue;
@ -543,17 +1014,7 @@
ctx.lineWidth = 2;
ctx.stroke();
} else if (this.isMapTransfer(event)) {
// Draw exit/entrance marker (eerie green glow)
ctx.fillStyle = '#7cfc00';
ctx.shadowColor = '#7cfc00';
ctx.shadowBlur = 6;
ctx.beginPath();
ctx.arc(ex, ey, markerSize, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#2a4a00';
ctx.lineWidth = 2;
ctx.stroke();
// Skip - transfers are drawn as numbered markers by createExitMarkers()
} else if (this.isImportantEvent(event)) {
// Draw NPC marker (warm gold)
ctx.fillStyle = '#daa520';
@ -828,17 +1289,23 @@
const playerX = $gamePlayer.x;
const playerY = $gamePlayer.y;
// Use current transform offsets for zoom/pan support
const offsetX = this._currentOffsetX || this._minimapOffsetX;
const offsetY = this._currentOffsetY || this._minimapOffsetY;
const scale = this._currentScale || this._minimapScale;
// Calculate marker position
const markerX = this._minimapOffsetX + (playerX + 0.5) * this._minimapTileWidth * this._minimapScale;
const markerY = this._minimapOffsetY + (playerY + 0.5) * this._minimapTileHeight * this._minimapScale;
const markerX = offsetX + (playerX + 0.5) * this._minimapTileWidth * scale;
const markerY = offsetY + (playerY + 0.5) * this._minimapTileHeight * scale;
this._minimapPlayerMarker.x = markerX;
this._minimapPlayerMarker.y = markerY;
// Pulse effect
// Pulse effect with zoom scaling
const pulse = Math.sin(Graphics.frameCount * 0.1) * 0.2 + 1;
this._minimapPlayerMarker.scale.x = pulse;
this._minimapPlayerMarker.scale.y = pulse;
const zoomScale = Math.max(0.5, minimapZoom * 0.8);
this._minimapPlayerMarker.scale.x = pulse * zoomScale;
this._minimapPlayerMarker.scale.y = pulse * zoomScale;
};
Scene_Map.prototype.removeMinimapOverlay = function() {
@ -846,6 +1313,10 @@
this.removeChild(this._minimapOverlay);
this._minimapOverlay = null;
}
if (this._minimapBorder) {
this.removeChild(this._minimapBorder);
this._minimapBorder = null;
}
if (this._minimapContent) {
this.removeChild(this._minimapContent);
this._minimapContent = null;
@ -854,6 +1325,16 @@
this.removeChild(this._minimapPlayerMarker);
this._minimapPlayerMarker = null;
}
if (this._exitMarkers) {
for (const marker of this._exitMarkers) {
this.removeChild(marker);
}
this._exitMarkers = null;
}
if (this._exitDestinationsPanel) {
this.removeChild(this._exitDestinationsPanel);
this._exitDestinationsPanel = null;
}
};
//=========================================================================