From c47f823c83d522f2b6a2fcbc9e6b6eb884325b4d Mon Sep 17 00:00:00 2001 From: dazedanon Date: Sun, 21 Dec 2025 11:59:06 -0600 Subject: [PATCH] Minimap with destinations --- js/plugins/MinimapZoom.js | 531 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 506 insertions(+), 25 deletions(-) diff --git a/js/plugins/MinimapZoom.js b/js/plugins/MinimapZoom.js index dc7ead8..cbbf6dc 100644 --- a/js/plugins/MinimapZoom.js +++ b/js/plugins/MinimapZoom.js @@ -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; + } }; //=========================================================================