unholy-maiden/js/plugins/MinimapZoom.js
2025-12-22 11:40:32 -06:00

1932 lines
77 KiB
JavaScript

//=============================================================================
// MinimapZoom.js
//=============================================================================
/*:
* @plugindesc Adds a minimap/zoom-out feature with exit labels, zoom and pan controls.
* @author Dazed Translations
*
* @param Toggle Key
* @desc The key to toggle minimap (keycode: M = 79)
* @default 79
*
* @help
* 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.
*
* Controls:
* - M: Close minimap
* - Mouse Wheel / Shift / Ctrl: Zoom in/out
* - Arrow Keys / 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
* - Fog of War: Only explored areas are revealed on the minimap
* - Exit Discovery: Exits are only shown after the player uses them
*
* Settings (Options Menu):
* - "Minimap Mod (M)": Enable/disable the minimap feature entirely
* - "Fog of War": Dark overlay hiding unexplored map areas
* - "Hide Exits": Hide exits/locations until discovered by the player
*
* Combinations:
* - Both ON: Full exploration mode (dark fog + hidden exits)
* - Fog OFF, Exits ON: See full map but exits hidden until used
* - Fog ON, Exits OFF: Dark fog but all exits visible
* - Both OFF: See everything immediately
*/
(function() {
'use strict';
// Plugin parameters
const pluginName = 'MinimapZoom';
const parameters = PluginManager.parameters(pluginName);
const toggleKeyCode = Number(parameters['Toggle Key'] || 79); // M key
// 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 = [];
// Fog of war - exploration tracking
const EXPLORE_RADIUS = 5; // Tiles revealed around player
// Export minimap state for other plugins/systems to check
window.isMinimapActive = function() {
return minimapActive;
};
// Function to close minimap from anywhere (used by event start)
window.closeMinimapIfActive = function() {
if (minimapActive && SceneManager._scene && SceneManager._scene.hideMinimap) {
minimapActive = false;
SceneManager._scene.hideMinimap();
SoundManager.playCancel();
}
};
//=========================================================================
// ConfigManager - Add minimap enabled setting
//=========================================================================
// Default values (enabled by default)
ConfigManager.minimapEnabled = true;
ConfigManager.minimapFogOfWar = true; // Fog of war (dark overlay) enabled by default
ConfigManager.minimapRevealAll = false; // Reveal all destinations OFF by default (exploration mode)
// Hook into makeData to save the settings
const _ConfigManager_makeData = ConfigManager.makeData;
ConfigManager.makeData = function() {
const config = _ConfigManager_makeData.call(this);
config.minimapEnabled = this.minimapEnabled;
config.minimapFogOfWar = this.minimapFogOfWar;
config.minimapRevealAll = this.minimapRevealAll;
return config;
};
// Hook into applyData to load the settings
const _ConfigManager_applyData = ConfigManager.applyData;
ConfigManager.applyData = function(config) {
_ConfigManager_applyData.call(this, config);
this.minimapEnabled = this.readFlag(config, "minimapEnabled", true);
this.minimapFogOfWar = this.readFlag(config, "minimapFogOfWar", true);
this.minimapRevealAll = this.readFlag(config, "minimapRevealAll", false);
};
//=========================================================================
// Window_Options - Add minimap toggle option
//=========================================================================
const _Window_Options_makeCommandList = Window_Options.prototype.makeCommandList;
Window_Options.prototype.makeCommandList = function() {
_Window_Options_makeCommandList.call(this);
this.addCommand("Minimap Mod (M)", "minimapEnabled");
this.addCommand(" Fog of War", "minimapFogOfWar");
this.addCommand(" Reveal Destinations", "minimapRevealAll");
};
//=========================================================================
// Input - Add minimap key and zoom/pan keys
//=========================================================================
Input.keyMapper[toggleKeyCode] = 'minimap';
Input.keyMapper[82] = 'minimapReset'; // R key
// Note: Shift (16) and Ctrl (17) are checked directly in updateMinimapZoomPan
// to avoid interfering with game controls when minimap is not active
//=========================================================================
// Auto-close minimap when any event starts
//=========================================================================
// When any event starts, auto-close the minimap
const _Game_Event_start = Game_Event.prototype.start;
Game_Event.prototype.start = function() {
// Close minimap if it's open when an event tries to start
window.closeMinimapIfActive();
_Game_Event_start.call(this);
};
//=========================================================================
// Exploration Tracking - Fog of War System
//=========================================================================
// Initialize exploration data on Game_System
const _Game_System_initialize = Game_System.prototype.initialize;
Game_System.prototype.initialize = function() {
_Game_System_initialize.call(this);
this._exploredMaps = {};
this._activatedExits = {}; // Track which exits have been used
};
// Get explored tiles for current map
Game_System.prototype.getExploredTiles = function(mapId) {
if (!this._exploredMaps) {
this._exploredMaps = {};
}
if (!this._exploredMaps[mapId]) {
this._exploredMaps[mapId] = {};
}
return this._exploredMaps[mapId];
};
// Mark a tile as explored
Game_System.prototype.exploreTile = function(mapId, x, y) {
const explored = this.getExploredTiles(mapId);
const key = x + ',' + y;
explored[key] = true;
};
// Check if a tile is explored
Game_System.prototype.isTileExplored = function(mapId, x, y) {
const explored = this.getExploredTiles(mapId);
const key = x + ',' + y;
return explored[key] === true;
};
// Mark an exit as activated (used by player)
Game_System.prototype.activateExit = function(fromMapId, toMapId, x, y) {
if (!this._activatedExits) {
this._activatedExits = {};
}
if (!this._activatedExits[fromMapId]) {
this._activatedExits[fromMapId] = {};
}
// Key by destination map and approximate position (to handle grouped exits)
const key = toMapId + '_' + Math.floor(x / 4) + '_' + Math.floor(y / 4);
this._activatedExits[fromMapId][key] = true;
// Also store exact position for precise matching
const exactKey = toMapId + '_' + x + '_' + y;
this._activatedExits[fromMapId][exactKey] = true;
};
// Check if an exit has been activated
Game_System.prototype.isExitActivated = function(fromMapId, toMapId, x, y) {
if (!this._activatedExits) {
this._activatedExits = {};
}
if (!this._activatedExits[fromMapId]) {
return false;
}
// Check exact position first
const exactKey = toMapId + '_' + x + '_' + y;
if (this._activatedExits[fromMapId][exactKey]) {
return true;
}
// Check grouped position
const key = toMapId + '_' + Math.floor(x / 4) + '_' + Math.floor(y / 4);
return this._activatedExits[fromMapId][key] === true;
};
// Reveal tiles around a position
Game_System.prototype.revealAroundPosition = function(mapId, centerX, centerY, radius) {
for (let dx = -radius; dx <= radius; dx++) {
for (let dy = -radius; dy <= radius; dy++) {
// Use circular radius
if (dx * dx + dy * dy <= radius * radius) {
const x = centerX + dx;
const y = centerY + dy;
if (x >= 0 && y >= 0 && x < $gameMap.width() && y < $gameMap.height()) {
this.exploreTile(mapId, x, y);
}
}
}
}
};
// Track when player uses a transfer command
const _Game_Interpreter_command201 = Game_Interpreter.prototype.command201;
Game_Interpreter.prototype.command201 = function(params) {
// params[0]: 0 = direct, 1 = variable designation
// params[1]: map ID (or variable ID)
// params[2]: x (or variable ID)
// params[3]: y (or variable ID)
// Get actual values (handle both direct and variable designation)
let toMapId, toX, toY;
if (params[0] === 0) {
toMapId = params[1];
toX = params[2];
toY = params[3];
} else {
toMapId = $gameVariables.value(params[1]);
toX = $gameVariables.value(params[2]);
toY = $gameVariables.value(params[3]);
}
const fromMapId = $gameMap.mapId();
// Use the event's position as the exit location
const event = $gameMap.event(this._eventId);
if (event) {
$gameSystem.activateExit(fromMapId, toMapId, event.x, event.y);
}
// Also record the arrival exit (where player comes out on destination map)
$gameSystem.activateExit(toMapId, fromMapId, toX, toY);
// Store info about where we came from so we can reveal the return exit
$gameSystem._pendingEntryReveal = {
destinationMapId: toMapId,
sourceMapId: fromMapId,
landingX: toX,
landingY: toY
};
return _Game_Interpreter_command201.call(this, params);
};
// Find nearby exit events and activate them (so they show on minimap)
Game_System.prototype.activateNearbyExits = function(mapId, targetMapId, nearX, nearY) {
for (const event of $gameMap.events()) {
if (!event) continue;
// Only check events within reasonable distance
const dx = Math.abs(event.x - nearX);
const dy = Math.abs(event.y - nearY);
if (dx > 15 || dy > 15) continue;
const eventData = event.event();
if (!eventData) continue;
// Check all pages for Transfer Player command leading to target map
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;
let cmdTargetMapId = params[0] === 0 ? params[1] : $gameVariables.value(params[1]);
if (cmdTargetMapId === targetMapId) {
// Reveal and activate this exit
this.revealAroundPosition(mapId, event.x, event.y, EXPLORE_RADIUS);
this.activateExit(mapId, targetMapId, event.x, event.y);
}
break;
}
}
}
}
};
// Track player movement to reveal tiles
const _Game_Player_moveStraight = Game_Player.prototype.moveStraight;
Game_Player.prototype.moveStraight = function(d) {
_Game_Player_moveStraight.call(this, d);
if (this.isMovementSucceeded()) {
$gameSystem.revealAroundPosition($gameMap.mapId(), this.x, this.y, EXPLORE_RADIUS);
}
};
// Also reveal on map setup (when entering a new map or loading)
const _Game_Player_performTransfer = Game_Player.prototype.performTransfer;
Game_Player.prototype.performTransfer = function() {
_Game_Player_performTransfer.call(this);
if ($gameMap.mapId() > 0) {
// Reveal around player's new position
$gameSystem.revealAroundPosition($gameMap.mapId(), this.x, this.y, EXPLORE_RADIUS);
// Note: Exit reveal is handled in Scene_Map.start after map is fully loaded
}
};
// Reveal on game load as well - and handle pending entry reveals
const _Scene_Map_start = Scene_Map.prototype.start;
Scene_Map.prototype.start = function() {
_Scene_Map_start.call(this);
if ($gameMap.mapId() > 0) {
$gameSystem.revealAroundPosition($gameMap.mapId(), $gamePlayer.x, $gamePlayer.y, EXPLORE_RADIUS);
// Check for pending entry reveal - find and activate nearby exits leading back
if ($gameSystem._pendingEntryReveal &&
$gameSystem._pendingEntryReveal.destinationMapId === $gameMap.mapId()) {
const entry = $gameSystem._pendingEntryReveal;
$gameSystem.activateNearbyExits(
$gameMap.mapId(),
entry.sourceMapId,
entry.landingX,
entry.landingY
);
$gameSystem._pendingEntryReveal = null;
}
}
};
//=========================================================================
// Scene_Map - Handle minimap toggle
//=========================================================================
const _Scene_Map_update = Scene_Map.prototype.update;
Scene_Map.prototype.update = function() {
_Scene_Map_update.call(this);
this.updateMinimapToggle();
};
Scene_Map.prototype.updateMinimapToggle = function() {
// Check if minimap is enabled in options
if (!ConfigManager.minimapEnabled) {
// If minimap is currently active but disabled, close it
if (minimapActive) {
minimapActive = false;
this.hideMinimap();
}
return; // Don't process minimap inputs when disabled
}
// Allow closing minimap anytime (even during events/messages), but only open when not busy
if (Input.isTriggered('minimap')) {
if (minimapActive) {
// Always allow closing
this.toggleMinimap();
} else if (!$gameMessage.isBusy() && !$gameMap.isEventRunning()) {
// Only open when not busy
this.toggleMinimap();
}
}
// Auto-close minimap if an event starts running or message appears
if (minimapActive && ($gameMap.isEventRunning() || $gameMessage.isBusy())) {
minimapActive = false;
this.hideMinimap();
SoundManager.playCancel();
}
// 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();
}
this.updateMinimapAtmosphere();
}
};
Scene_Map.prototype.collectExitLocations = function() {
exitLocations = [];
const rawExits = [];
const mapId = $gameMap.mapId();
// Collect all transfer events (show all if revealAll is ON, otherwise filter by exploration)
const revealAll = ConfigManager.minimapRevealAll;
for (const event of $gameMap.events()) {
if (!event) continue;
// Only include exits in explored areas (unless revealAll is enabled)
if (!revealAll && !$gameSystem.isTileExplored(mapId, event.x, event.y)) {
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];
// Only show exits that have been activated (used by player) - unless revealAll is enabled
if (!revealAll && !$gameSystem.isExitActivated(mapId, targetMapId, event.x, event.y)) {
continue;
}
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 - check raw key state directly only when minimap is active
// This prevents Shift/Ctrl from being captured when minimap is closed
const shiftPressed = Input._currentState['shift'];
const ctrlPressed = Input._currentState['control'];
if (shiftPressed) {
minimapZoom = Math.min(MAX_ZOOM, minimapZoom + ZOOM_STEP);
zoomChanged = true;
}
if (ctrlPressed) {
minimapZoom = Math.max(MIN_ZOOM, minimapZoom - ZOOM_STEP);
zoomChanged = true;
}
// Pan controls (arrow keys)
if (Input.isPressed('up')) {
minimapPanY += PAN_SPEED;
panChanged = true;
}
if (Input.isPressed('down')) {
minimapPanY -= PAN_SPEED;
panChanged = true;
}
if (Input.isPressed('left')) {
minimapPanX += PAN_SPEED;
panChanged = true;
}
if (Input.isPressed('right')) {
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 (right side)
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;
}
// Update legend panel to stay next to map (left side)
if (this._legendPanel) {
this._legendPanel.scale.x = minimapZoom;
this._legendPanel.scale.y = minimapZoom;
const legendWidth = 120 * minimapZoom;
this._legendPanel.x = baseOffsetX + minimapPanX - legendWidth - this._legendPanelOffset * minimapZoom;
this._legendPanel.y = baseOffsetY + minimapPanY;
}
// Update title panel to stay above map (centered)
if (this._titlePanel) {
this._titlePanel.scale.x = minimapZoom;
this._titlePanel.scale.y = minimapZoom;
this._titlePanel.x = baseOffsetX + minimapPanX + scaledWidth / 2;
this._titlePanel.y = baseOffsetY + minimapPanY - this._titlePanelOffset * minimapZoom;
}
// Update controls panel to stay below map (centered)
if (this._controlsPanel) {
this._controlsPanel.scale.x = minimapZoom;
this._controlsPanel.scale.y = minimapZoom;
this._controlsPanel.x = baseOffsetX + minimapPanX + scaledWidth / 2;
this._controlsPanel.y = baseOffsetY + minimapPanY + scaledHeight + this._controlsPanelOffset * minimapZoom;
}
// 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) {
const flicker = 0.92 + Math.sin(Graphics.frameCount * 0.05) * 0.03 +
Math.sin(Graphics.frameCount * 0.13) * 0.02;
this._minimapContent.opacity = Math.floor(255 * flicker);
}
};
Scene_Map.prototype.toggleMinimap = function() {
minimapActive = !minimapActive;
if (minimapActive) {
this.showMinimap();
SoundManager.playCursor();
} else {
this.hideMinimap();
SoundManager.playCancel();
}
};
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;
// Create minimap overlay
this.createMinimapOverlay();
};
Scene_Map.prototype.hideMinimap = function() {
if (!this._spriteset) return;
// Show the normal spriteset
this._spriteset.visible = true;
// Remove minimap overlay
this.removeMinimapOverlay();
};
Scene_Map.prototype.createMinimapOverlay = function() {
if (this._minimapOverlay) return;
// Create container for minimap
this._minimapOverlay = new Sprite();
this._minimapOverlay.bitmap = new Bitmap(Graphics.width, Graphics.height);
// Draw dark atmospheric background with vignette
this.drawAtmosphericBackground();
// Get map dimensions
const mapWidth = $gameMap.width();
const mapHeight = $gameMap.height();
const tileWidth = $gameMap.tileWidth();
const tileHeight = $gameMap.tileHeight();
// Calculate scale to fit entire map on screen with padding
const padding = 70;
const availableWidth = Graphics.width - padding * 2;
const availableHeight = Graphics.height - padding * 2;
const mapPixelWidth = mapWidth * tileWidth;
const mapPixelHeight = mapHeight * tileHeight;
const scaleX = availableWidth / mapPixelWidth;
const scaleY = availableHeight / mapPixelHeight;
const scale = Math.min(scaleX, scaleY, 1); // Don't zoom in past 1x
const scaledWidth = mapPixelWidth * scale;
const scaledHeight = mapPixelHeight * scale;
// Center the map
const offsetX = (Graphics.width - scaledWidth) / 2;
const offsetY = (Graphics.height - scaledHeight) / 2;
// Store for marker positioning
this._minimapScale = scale;
this._minimapOffsetX = offsetX;
this._minimapOffsetY = offsetY;
this._minimapTileWidth = tileWidth;
this._minimapTileHeight = tileHeight;
// Create the map snapshot sprite
this._minimapContent = new Sprite();
this._minimapContent.x = offsetX;
this._minimapContent.y = offsetY;
this._minimapContent.scale.x = scale;
this._minimapContent.scale.y = scale;
// Render the tilemap to a bitmap
this.renderMinimapTiles();
// Create border as separate sprite that moves with content
this.createMinimapBorder(mapPixelWidth, mapPixelHeight);
this.addChild(this._minimapOverlay);
this.addChild(this._minimapBorder);
this.addChild(this._minimapContent);
// Create title panel (moves with map) - at top
this.createTitlePanel(mapPixelWidth, mapPixelHeight);
// Create controls panel (moves with map) - at bottom
this.createControlsPanel(mapPixelWidth, mapPixelHeight);
// Create legend panel (moves with map) - on left side
this.createLegendPanel(mapPixelWidth, mapPixelHeight);
// Create exit destinations panel (moves with map) - on right side
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() {
const bitmap = this._minimapOverlay.bitmap;
const ctx = bitmap.context;
const w = Graphics.width;
const h = Graphics.height;
// Base dark parchment color
ctx.fillStyle = '#1a1410';
ctx.fillRect(0, 0, w, h);
// Add subtle parchment texture noise
for (let i = 0; i < 8000; i++) {
const x = Math.random() * w;
const y = Math.random() * h;
const alpha = Math.random() * 0.08;
const shade = Math.floor(Math.random() * 30) + 20;
ctx.fillStyle = `rgba(${shade}, ${shade - 5}, ${shade - 10}, ${alpha})`;
ctx.fillRect(x, y, 1, 1);
}
// Add vignette effect (darker edges)
const gradient = ctx.createRadialGradient(w/2, h/2, h * 0.3, w/2, h/2, h * 0.8);
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
gradient.addColorStop(0.7, 'rgba(0, 0, 0, 0.3)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.7)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
};
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';
ctx.lineWidth = borderSize + 4;
ctx.strokeRect(x - borderSize - 2, y - borderSize - 2, width + (borderSize + 2) * 2, height + (borderSize + 2) * 2);
// Main gold border
ctx.strokeStyle = '#8b7355';
ctx.lineWidth = borderSize;
ctx.strokeRect(x - borderSize/2, y - borderSize/2, width + borderSize, height + borderSize);
// Inner highlight
ctx.strokeStyle = '#c9a959';
ctx.lineWidth = 1;
ctx.strokeRect(x - 1, y - 1, width + 2, height + 2);
// Corner ornaments
const cornerSize = 12;
ctx.fillStyle = '#d4af37';
// Top-left corner
ctx.beginPath();
ctx.arc(x, y, cornerSize/2, 0, Math.PI * 2);
ctx.fill();
// Top-right corner
ctx.beginPath();
ctx.arc(x + width, y, cornerSize/2, 0, Math.PI * 2);
ctx.fill();
// Bottom-left corner
ctx.beginPath();
ctx.arc(x, y + height, cornerSize/2, 0, Math.PI * 2);
ctx.fill();
// Bottom-right corner
ctx.beginPath();
ctx.arc(x + width, y + height, cornerSize/2, 0, Math.PI * 2);
ctx.fill();
// Inner corner dots
ctx.fillStyle = '#1a0a00';
const innerDot = 4;
ctx.beginPath();
ctx.arc(x, y, innerDot/2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + width, y, innerDot/2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x, y + height, innerDot/2, 0, Math.PI * 2);
ctx.fill();
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;
};
Scene_Map.prototype.createTitlePanel = function(mapPixelWidth, mapPixelHeight) {
const panelWidth = 350;
const panelHeight = 65;
// Create title sprite
this._titlePanel = new Sprite();
this._titlePanel.bitmap = new Bitmap(panelWidth, panelHeight);
const bitmap = this._titlePanel.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 = 26;
bitmap.textColor = '#d4af37';
bitmap.outlineColor = '#1a0a00';
bitmap.outlineWidth = 4;
// Main title
bitmap.drawText('~ Cartograph ~', 0, 8, panelWidth, 30, 'center');
// Map name
const mapName = $gameMap.displayName() || 'Unknown Region';
bitmap.fontSize = 18;
bitmap.textColor = '#c9a959';
bitmap.drawText('― ' + mapName + ' ―', 0, 38, panelWidth, 22, 'center');
// Position panel above the map (centered)
this._titlePanelOffset = 15; // Gap between map and panel
this._titlePanel.anchor = new Point(0.5, 1); // Anchor at bottom center
this._titlePanel.x = this._minimapOffsetX + (mapPixelWidth * this._minimapScale) / 2;
this._titlePanel.y = this._minimapOffsetY - this._titlePanelOffset;
this.addChild(this._titlePanel);
};
Scene_Map.prototype.createControlsPanel = function(mapPixelWidth, mapPixelHeight) {
const panelWidth = 500;
const panelHeight = 35;
// Create controls sprite
this._controlsPanel = new Sprite();
this._controlsPanel.bitmap = new Bitmap(panelWidth, panelHeight);
const bitmap = this._controlsPanel.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 = '#8b7355';
bitmap.outlineColor = '#1a0a00';
bitmap.outlineWidth = 3;
// Controls text
bitmap.drawText('M: Close | Arrows: Pan | Shift/Ctrl/Scroll: Zoom | R: Reset', 0, 8, panelWidth, 20, 'center');
// Position panel below the map (centered)
this._controlsPanelOffset = 15; // Gap between map and panel
this._controlsPanel.anchor = new Point(0.5, 0); // Anchor at top center
this._controlsPanel.x = this._minimapOffsetX + (mapPixelWidth * this._minimapScale) / 2;
this._controlsPanel.y = this._minimapOffsetY + (mapPixelHeight * this._minimapScale) + this._controlsPanelOffset;
this.addChild(this._controlsPanel);
};
// 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.createLegendPanel = function(mapPixelWidth, mapPixelHeight) {
const dotSize = 10;
const lineHeight = 24;
const padding = 15;
const panelWidth = 120;
const panelHeight = 35 + 5 * lineHeight + padding;
// Create legend sprite
this._legendPanel = new Sprite();
this._legendPanel.bitmap = new Bitmap(panelWidth, panelHeight);
const bitmap = this._legendPanel.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('~ Legend ~', 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 = '#a89070';
const startY = 32;
const legendX = padding;
// Player marker
ctx.fillStyle = '#cc0000';
ctx.beginPath();
ctx.arc(legendX + dotSize/2, startY + dotSize/2 + 5, dotSize/2, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ffcc00';
ctx.lineWidth = 2;
ctx.stroke();
bitmap.drawText('You', legendX + dotSize + 8, startY + 2, 80, 18, 'left');
// NPC marker (warm gold)
ctx.fillStyle = '#daa520';
ctx.beginPath();
ctx.arc(legendX + dotSize/2, startY + lineHeight + dotSize/2 + 5, dotSize/2, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#2a1a00';
ctx.lineWidth = 1;
ctx.stroke();
bitmap.drawText('NPC', legendX + dotSize + 8, startY + lineHeight + 2, 80, 18, 'left');
// Locked door/chest marker (teal/mysterious)
ctx.fillStyle = '#40e0d0';
ctx.beginPath();
ctx.arc(legendX + dotSize/2, startY + lineHeight * 2 + dotSize/2 + 5, dotSize/2, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#2a1a00';
ctx.lineWidth = 1;
ctx.stroke();
bitmap.drawText('Locked', legendX + dotSize + 8, startY + lineHeight * 2 + 2, 80, 18, 'left');
// Enemy marker (blood red)
ctx.fillStyle = '#dc143c';
ctx.beginPath();
ctx.arc(legendX + dotSize/2, startY + lineHeight * 3 + dotSize/2 + 5, dotSize/2, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#2a1a00';
ctx.lineWidth = 1;
ctx.stroke();
bitmap.drawText('Enemy', legendX + dotSize + 8, startY + lineHeight * 3 + 2, 80, 18, 'left');
// Exit/Transfer marker (eerie green)
ctx.fillStyle = '#7cfc00';
ctx.beginPath();
ctx.arc(legendX + dotSize/2, startY + lineHeight * 4 + dotSize/2 + 5, dotSize/2, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#2a1a00';
ctx.lineWidth = 1;
ctx.stroke();
bitmap.drawText('Passage', legendX + dotSize + 8, startY + lineHeight * 4 + 2, 80, 18, 'left');
// Position panel to the left of the map
this._legendPanelOffset = 20; // Gap between map and panel
this._legendPanel.x = this._minimapOffsetX - panelWidth - this._legendPanelOffset;
this._legendPanel.y = this._minimapOffsetY;
this.addChild(this._legendPanel);
};
// Keep old function for compatibility
Scene_Map.prototype.drawMinimapLegend = function() {
// Now handled by createLegendPanel
};
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();
const tileWidth = $gameMap.tileWidth();
const tileHeight = $gameMap.tileHeight();
// Create bitmap for the map
const bitmap = new Bitmap(mapWidth * tileWidth, mapHeight * tileHeight);
this._minimapContent.bitmap = bitmap;
// Get tileset
const tileset = $gameMap.tileset();
if (!tileset) return;
// Dark fantasy color palette
const wallColor = '#1a1410'; // Very dark brown/black for walls
const floorColor = '#5a4f45'; // Lighter dusty floor (more contrast)
const floorAltColor = '#4d4238'; // Floor variation
// Simple tile rendering - draw colored rectangles based on passability
for (let y = 0; y < mapHeight; y++) {
for (let x = 0; x < mapWidth; x++) {
const px = x * tileWidth;
const py = y * tileHeight;
// Check tile properties - use checkPassage for more accurate detection
// checkPassage checks the actual tile bits rather than just directional passability
const bit = 0x0f; // All directions
const passage = $gameMap.checkPassage(x, y, bit);
// Also check standard passability as backup
const dirPassable = $gameMap.isPassable(x, y, 2) ||
$gameMap.isPassable(x, y, 4) ||
$gameMap.isPassable(x, y, 6) ||
$gameMap.isPassable(x, y, 8);
// Check if any tile at this location is a "void" tile (like pits)
// by examining the actual tile IDs
const tiles = $gameMap.allTiles(x, y);
let hasFloorTile = false;
// Look for actual floor/ground tiles
// RPG Maker MZ Tileset Layout:
// A1 (animated/water): 2048-2815 - water, waterfalls, poison swamps, lava
// A2 (ground): 2816-4351 - grass, dirt, stone floors
// A3 (buildings): 4352-5887 - roofs and building exteriors
// A4 (walls): 5888-8191 - wall tops
// A5 (normal tiles): 1536-1663 - simple ground tiles
// B-E tiles: start at different offsets for decoration
for (const tileId of tiles) {
if (tileId === 0) continue;
// A5 tiles (simple ground): 1536-1663
if (tileId >= 1536 && tileId < 1664) {
hasFloorTile = true;
break;
}
// A1 tiles (water/animated - including poison, lava, swamps): 2048-2815
if (tileId >= 2048 && tileId < 2816) {
hasFloorTile = true;
break;
}
// A2 tiles (ground autotiles): 2816-4351
if (tileId >= 2816 && tileId < 4352) {
hasFloorTile = true;
break;
}
// B-E tiles can also be walkable floors (tile ID encoding varies)
// These are typically in lower ranges and used for details/overlays
// If a B-E tile is passable, it should be considered ground
if (tileId > 0 && tileId < 1536) {
// This is a B-E tile, check if it's passable via flags
const flags = $gameMap.tilesetFlags();
const flag = flags[tileId];
if (flag !== undefined && (flag & 0x10) === 0) {
// Not star passability (not an overlay) - could be floor
if ((flag & 0x0F) !== 0x0F) {
// Not fully blocked
hasFloorTile = true;
break;
}
}
}
}
// Consider passable only if: passage check passes AND has a floor tile
// OR if standard directional passability says it's passable AND has floor
const isPassable = (passage || dirPassable) && hasFloorTile;
// Get terrain tag for variety
const terrainTag = $gameMap.terrainTag(x, y);
const regionId = $gameMap.regionId(x, y);
// Color based on passability and terrain - dark fantasy palette
let color;
if (!isPassable) {
// Impassable - dark stone walls
color = wallColor;
// Add slight variation
if ((x * 7 + y * 13) % 5 === 0) {
color = '#352a24';
}
} else if (terrainTag > 0) {
// Special terrain - different dungeon features
const terrainColors = [
'#3d4a3d', // Mossy
'#4a4a3d', // Sandy
'#3d3d4a', // Damp stone
'#4a3d3d', // Bloodstained
'#3d4a4a', // Flooded
'#4a3d4a', // Corrupted
'#4a4a4a' // Ashen
];
color = terrainColors[(terrainTag - 1) % terrainColors.length];
} else {
// Normal passable floor - aged stone
color = (x + y) % 2 === 0 ? floorColor : floorAltColor;
}
// Add noise for texture
const noise = ((x * 17 + y * 31) % 10) - 5;
color = this.adjustColor(color, noise);
bitmap.fillRect(px, py, tileWidth, tileHeight, color);
// Draw subtle worn grid lines (like old stone tiles)
const ctx = bitmap.context;
ctx.strokeStyle = 'rgba(0, 0, 0, 0.15)';
ctx.lineWidth = 1;
ctx.strokeRect(px, py, tileWidth, tileHeight);
// Add occasional cracks/details on floors
if (isPassable && (x * 23 + y * 37) % 20 === 0) {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)';
ctx.beginPath();
ctx.moveTo(px + 2, py + 2);
ctx.lineTo(px + tileWidth - 4, py + tileHeight - 4);
ctx.stroke();
}
}
}
// Draw important events as markers (except transfers - those are drawn as numbered markers separately)
const revealAllMarkers = ConfigManager.minimapRevealAll;
const fogOfWarEnabled = ConfigManager.minimapFogOfWar;
const mapId = $gameMap.mapId();
for (const event of $gameMap.events()) {
if (!event) continue;
const isExplored = $gameSystem.isTileExplored(mapId, event.x, event.y);
const isEnemy = this.isEnemyEvent(event);
// Enemies: visible if fog of war is off OR tile is explored
// Other markers: visible if revealAll is on OR tile is explored
if (isEnemy) {
if (fogOfWarEnabled && !isExplored) continue;
} else {
if (!revealAllMarkers && !isExplored) continue;
}
const ex = event.x * tileWidth + tileWidth / 2;
const ey = event.y * tileHeight + tileHeight / 2;
const ctx = bitmap.context;
const markerSize = Math.min(tileWidth, tileHeight) / 2.5;
// Check event types in priority order
if (this.isLockedDoorOrChest(event)) {
// Draw locked door/chest marker (teal/mysterious)
ctx.fillStyle = '#40e0d0';
ctx.shadowColor = '#40e0d0';
ctx.shadowBlur = 4;
ctx.beginPath();
ctx.arc(ex, ey, markerSize, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#1a3a3a';
ctx.lineWidth = 2;
ctx.stroke();
} else if (this.isMapTransfer(event)) {
// Skip - transfers are drawn as numbered markers by createExitMarkers()
} else if (this.isEnemyEvent(event)) {
// Draw enemy marker (blood red)
ctx.fillStyle = '#dc143c';
ctx.shadowColor = '#dc143c';
ctx.shadowBlur = 4;
ctx.beginPath();
ctx.arc(ex, ey, markerSize, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#4a0a0a';
ctx.lineWidth = 2;
ctx.stroke();
} else if (this.isImportantEvent(event)) {
// Draw NPC marker (warm gold)
ctx.fillStyle = '#daa520';
ctx.shadowColor = '#daa520';
ctx.shadowBlur = 3;
ctx.beginPath();
ctx.arc(ex, ey, markerSize, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#4a3500';
ctx.lineWidth = 2;
ctx.stroke();
}
}
// Draw fog of war over unexplored areas (if enabled)
if (ConfigManager.minimapFogOfWar) {
this.drawFogOfWar(bitmap, mapWidth, mapHeight, tileWidth, tileHeight);
}
};
Scene_Map.prototype.drawFogOfWar = function(bitmap, mapWidth, mapHeight, tileWidth, tileHeight) {
const ctx = bitmap.context;
const mapId = $gameMap.mapId();
for (let y = 0; y < mapHeight; y++) {
for (let x = 0; x < mapWidth; x++) {
if (!$gameSystem.isTileExplored(mapId, x, y)) {
const px = x * tileWidth;
const py = y * tileHeight;
// Draw dark fog
ctx.fillStyle = '#0a0806';
ctx.fillRect(px, py, tileWidth, tileHeight);
// Add some texture/noise to the fog
if ((x + y) % 3 === 0) {
ctx.fillStyle = 'rgba(20, 15, 10, 0.5)';
ctx.fillRect(px, py, tileWidth, tileHeight);
}
} else {
// Check for partially explored edges (adjacent to unexplored)
const hasUnexploredNeighbor =
(x > 0 && !$gameSystem.isTileExplored(mapId, x - 1, y)) ||
(x < mapWidth - 1 && !$gameSystem.isTileExplored(mapId, x + 1, y)) ||
(y > 0 && !$gameSystem.isTileExplored(mapId, x, y - 1)) ||
(y < mapHeight - 1 && !$gameSystem.isTileExplored(mapId, x, y + 1));
if (hasUnexploredNeighbor) {
// Draw subtle edge shadow
const px = x * tileWidth;
const py = y * tileHeight;
ctx.fillStyle = 'rgba(10, 8, 6, 0.3)';
ctx.fillRect(px, py, tileWidth, tileHeight);
}
}
}
}
};
// Check if an event is a locked door, chest, or barred gate
Scene_Map.prototype.isLockedDoorOrChest = function(event) {
if (!event) return false;
// Get the event data
const eventData = event.event();
if (!eventData) return false;
const name = eventData.name;
// Check for locked door patterns
if (name.includes('鍵扉')) return true; // "Locked door"
// Check for chest patterns
if (name.includes('宝箱')) return true; // "Treasure chest"
// Check for barred door/gate patterns (iron bars)
if (name.includes('鉄格子')) return true; // "Iron bars/Barred gate"
// Check for barred/latch doors (opens from above/below pattern)
if (name.includes('上から') && name.includes('下から')) return true; // "From above... from below" latch doors
return false;
};
// Check if an event transfers to another map
Scene_Map.prototype.isMapTransfer = function(event) {
if (!event) return false;
// Get the event data
const eventData = event.event();
if (!eventData) return false;
const name = eventData.name;
// Check for transfer event name patterns
if (name.includes('移動')) return true; // "Transfer/Move"
// Check the current page for Transfer Player command (code 201)
const page = event.page();
if (!page) return false;
const list = page.list;
if (!list) return false;
for (const cmd of list) {
if (cmd.code === 201) { // Transfer Player command
return true;
}
}
return false;
};
// Check if an event is a locked door that can be picked (legacy - kept for compatibility)
Scene_Map.prototype.isLockedDoor = function(event) {
return this.isLockedDoorOrChest(event);
};
// Check if an event is an enemy (triggers battle)
Scene_Map.prototype.isEnemyEvent = function(event) {
if (!event) return false;
// Get the current active page - if no page is active, event is not visible
const page = event.page();
if (!page) return false;
// Must have a character graphic on the current page (enemy is visible)
if (!page.image || !page.image.characterName) return false;
// Get the event data
const eventData = event.event();
if (!eventData) return false;
// Check for <EnemySymbol> tag in the event note (this game's primary enemy marker)
const note = eventData.note || '';
if (note.includes('<EnemySymbol>')) return true;
// Check event name for specific enemy patterns used in this game
const name = eventData.name;
if (name.includes('エネミーシンボル')) return true; // "Enemy Symbol"
if (name.includes('ボス') && !name.includes('移動')) return true; // "Boss" but not transfer events
// Check the current page for Battle Processing command (code 301)
const list = page.list;
if (!list) return false;
for (const cmd of list) {
if (cmd.code === 301) { // Battle Processing command
return true;
}
}
return false;
};
// Determine if an event is "important" (NPC, not decoration/ambient)
Scene_Map.prototype.isImportantEvent = function(event) {
if (!event) return false;
// Must have a character graphic
if (!event.characterName()) return false;
// Get the event data
const eventData = event.event();
if (!eventData) return false;
// Check the current page
const page = event.page();
if (!page) return false;
// Skip if no trigger (parallel process/autorun without interaction)
// Trigger types: 0=Action Button, 1=Player Touch, 2=Event Touch, 3=Autorun, 4=Parallel
const trigger = page.trigger;
// Only show Action Button (0) and Player Touch (1) - these are interactive NPCs
if (trigger !== 0 && trigger !== 1) return false;
// Skip common decoration/ambient event names
const name = eventData.name.toLowerCase();
const skipPatterns = [
'窓', 'window', // Windows
'鳩', 'bird', 'pigeon', // Birds
'猫', 'cat', // Cats
'犬', 'dog', // Dogs
'light', '灯', 'lamp', // Lights
'flame', '炎', 'fire', // Fire effects
'smoke', '煙', // Smoke
'effect', '効果', // Effects
'particle', // Particles
'ambient', // Ambient
'decor', '装飾', // Decorations
'bg', 'background', // Background elements
'fog', '霧', // Fog
'water', '水', // Water effects
'shadow', '影', // Shadows
'grass', '草', // Grass
'tree', '木', // Trees
'flower', '花', // Flowers
'rock', '岩', // Rocks
'sparkle', 'shine', // Sparkle effects
'animation', 'anim', // Animations
];
for (const pattern of skipPatterns) {
if (name.includes(pattern)) return false;
}
// Skip events with very short command lists (likely just empty or simple triggers)
const list = page.list;
if (!list || list.length <= 2) return false;
// Check if event has meaningful commands (text, choices, transfers, etc.)
// Command codes: 101=Show Text, 102=Show Choices, 201=Transfer Player,
// 121=Control Switches, 122=Control Variables, 301=Battle Processing
const meaningfulCodes = [101, 102, 103, 104, 105, 201, 301, 302, 303, 311, 312, 326];
let hasMeaningfulCommand = false;
for (const cmd of list) {
if (meaningfulCodes.includes(cmd.code)) {
hasMeaningfulCommand = true;
break;
}
}
return hasMeaningfulCommand;
};
Scene_Map.prototype.isTilePassable = function(x, y) {
// First check: is this tile visually a "ceiling/void" tile?
// These are often star-passable but should show as walls on minimap
const tiles = $gameMap.allTiles(x, y);
const flags = $gameMap.tilesetFlags();
for (const tileId of tiles) {
if (tileId === 0) continue;
// A4 wall-top tiles (2048-2815) - these are wall tops/ceilings
// A5 floor tiles start at 1536, A4 starts at 2048
if (tileId >= 2048 && tileId < 2816) {
// This is an A4 wall-top tile - treat as impassable for minimap
return false;
}
// Check for tiles in the B-E range that have star passability
// Star passable (0x10) tiles that are in upper layers are usually ceilings/walls
if (tileId >= 256) { // B-E tiles
const flag = flags[tileId];
if (flag !== undefined && (flag & 0x10) !== 0) {
// Star passable - this is likely a ceiling tile, treat as wall
// Unless it's actually ground level
const layerTiles = $gameMap.layeredTiles(x, y);
if (layerTiles.length > 0 && layerTiles[0] === tileId) {
// This star tile is on the top layer - it's a ceiling/wall
return false;
}
}
}
}
// Check standard directional passability (all 4 directions)
const passableDown = $gameMap.isPassable(x, y, 2);
const passableLeft = $gameMap.isPassable(x, y, 4);
const passableRight = $gameMap.isPassable(x, y, 6);
const passableUp = $gameMap.isPassable(x, y, 8);
// If any direction is passable, consider it a floor
if (passableDown || passableLeft || passableRight || passableUp) {
// But also check if there's a blocking event at this location
const events = $gameMap.eventsXy(x, y);
for (const event of events) {
if (event && !event.isThrough() && event.isNormalPriority()) {
return false;
}
}
return true;
}
return false;
};
Scene_Map.prototype.adjustColor = function(hex, amount) {
// Simple color adjustment
let r = parseInt(hex.slice(1, 3), 16);
let g = parseInt(hex.slice(3, 5), 16);
let b = parseInt(hex.slice(5, 7), 16);
r = Math.min(255, Math.max(0, r + amount));
g = Math.min(255, Math.max(0, g + amount));
b = Math.min(255, Math.max(0, b + amount));
return '#' + r.toString(16).padStart(2, '0') +
g.toString(16).padStart(2, '0') +
b.toString(16).padStart(2, '0');
};
Scene_Map.prototype.createMinimapPlayerMarker = function() {
if (this._minimapPlayerMarker) return;
// Create player marker sprite - mystical/atmospheric style
this._minimapPlayerMarker = new Sprite();
this._minimapPlayerMarker.bitmap = new Bitmap(36, 36);
const ctx = this._minimapPlayerMarker.bitmap.context;
const cx = 18, cy = 18;
// Ethereal outer glow - deep crimson
const outerGlow = ctx.createRadialGradient(cx, cy, 0, cx, cy, 16);
outerGlow.addColorStop(0, 'rgba(139, 69, 19, 0)');
outerGlow.addColorStop(0.5, 'rgba(139, 69, 19, 0.3)');
outerGlow.addColorStop(1, 'rgba(139, 69, 19, 0)');
ctx.fillStyle = outerGlow;
ctx.beginPath();
ctx.arc(cx, cy, 16, 0, Math.PI * 2);
ctx.fill();
// Main marker - mystical blood red
const mainGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, 10);
mainGrad.addColorStop(0, '#c41e3a');
mainGrad.addColorStop(0.7, '#8b0000');
mainGrad.addColorStop(1, '#4a0000');
ctx.fillStyle = mainGrad;
ctx.beginPath();
ctx.arc(cx, cy, 10, 0, Math.PI * 2);
ctx.fill();
// Inner glow - warm amber
const innerGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, 6);
innerGrad.addColorStop(0, '#ffd700');
innerGrad.addColorStop(0.5, '#daa520');
innerGrad.addColorStop(1, '#b8860b');
ctx.fillStyle = innerGrad;
ctx.beginPath();
ctx.arc(cx, cy, 6, 0, Math.PI * 2);
ctx.fill();
// Center soul light
ctx.fillStyle = '#fff8dc';
ctx.beginPath();
ctx.arc(cx, cy, 2, 0, Math.PI * 2);
ctx.fill();
this._minimapPlayerMarker.anchor.x = 0.5;
this._minimapPlayerMarker.anchor.y = 0.5;
this.addChild(this._minimapPlayerMarker);
this.updateMinimapPlayerMarker();
};
Scene_Map.prototype.updateMinimapPlayerMarker = function() {
if (!this._minimapPlayerMarker) return;
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 = 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 with zoom scaling
const pulse = Math.sin(Graphics.frameCount * 0.1) * 0.2 + 1;
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() {
if (this._minimapOverlay) {
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;
}
if (this._minimapPlayerMarker) {
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;
}
if (this._legendPanel) {
this.removeChild(this._legendPanel);
this._legendPanel = null;
}
if (this._titlePanel) {
this.removeChild(this._titlePanel);
this._titlePanel = null;
}
if (this._controlsPanel) {
this.removeChild(this._controlsPanel);
this._controlsPanel = null;
}
};
//=========================================================================
// Handle scene transitions - Reset minimap when leaving map
//=========================================================================
const _Scene_Map_terminate = Scene_Map.prototype.terminate;
Scene_Map.prototype.terminate = function() {
if (minimapActive) {
this.hideMinimap();
minimapActive = false;
}
_Scene_Map_terminate.call(this);
};
//=========================================================================
// Prevent menu/actions while minimap is active
//=========================================================================
const _Scene_Map_isMenuEnabled = Scene_Map.prototype.isMenuEnabled;
Scene_Map.prototype.isMenuEnabled = function() {
if (minimapActive) return false;
return _Scene_Map_isMenuEnabled.call(this);
};
const _Scene_Map_isMapTouchOk = Scene_Map.prototype.isMapTouchOk;
Scene_Map.prototype.isMapTouchOk = function() {
if (minimapActive) return false;
return _Scene_Map_isMapTouchOk.call(this);
};
const _Game_Player_canMove = Game_Player.prototype.canMove;
Game_Player.prototype.canMove = function() {
if (minimapActive) return false;
return _Game_Player_canMove.call(this);
};
// Public accessor for minimap state
Scene_Map.prototype.isMinimapActive = function() {
return minimapActive;
};
})();