/*: * @target MZ * @plugindesc Full stitched map screenshot with optional exit labels/boxes and robust capture logic. Options toggle at bottom. Press O. NW.js only. * * @help * Features: * - Full map screenshot with multi-segment stitching * - Optional exit labels and boxes * - Robust recheck to ensure no segments are missing * - Camera restored after capture * - Saves as JPEG in "(FullMapScreenshot)" * - Options toggle at bottom */ (() => { const KEY_O = 79; const FRAME_WAIT = 5; const CROP_TOP_FRAC = 0.15; const CROP_RIGHT_FRAC = 0.35; const BOX_X_OFFSET = -4; const BOX_Y_OFFSET = -2; const MAX_CHARS_PER_LINE = 14; const LINE_HEIGHT = 24; let capturing = false; // --- ConfigManager defaults --- if (ConfigManager.mapScreenshotsEnabled === undefined) ConfigManager.mapScreenshotsEnabled = false; if (ConfigManager.mapScreenshotsShowLabels === undefined) ConfigManager.mapScreenshotsShowLabels = true; const _ConfigManager_makeData = ConfigManager.makeData; ConfigManager.makeData = function() { const config = _ConfigManager_makeData.call(this); config.mapScreenshotsEnabled = this.mapScreenshotsEnabled; config.mapScreenshotsShowLabels = this.mapScreenshotsShowLabels; return config; }; const _ConfigManager_applyData = ConfigManager.applyData; ConfigManager.applyData = function(config) { _ConfigManager_applyData.call(this, config); this.mapScreenshotsEnabled = config.mapScreenshotsEnabled ?? false; this.mapScreenshotsShowLabels = config.mapScreenshotsShowLabels ?? true; }; // --- Options menu patch --- const _Scene_Boot_start = Scene_Boot.prototype.start; Scene_Boot.prototype.start = function() { _Scene_Boot_start.call(this); if (!Window_Options.prototype._mapScreenshotPatched) { patchOptionsWindow(); Window_Options.prototype._mapScreenshotPatched = true; } }; function patchOptionsWindow() { const _Window_Options_makeCommandList = Window_Options.prototype.makeCommandList; Window_Options.prototype.makeCommandList = function() { _Window_Options_makeCommandList.call(this); if (!this._list.some(c => c.symbol === "mapScreenshotsEnabled")) { this.addCommand("Map Screenshot Mod (O)", "mapScreenshotsEnabled"); } if (!this._list.some(c => c.symbol === "mapScreenshotsShowLabels")) { this.addCommand(" Show Labels/Boxes", "mapScreenshotsShowLabels"); } }; } // --- Hotkey --- document.addEventListener("keydown", async e => { if ( e.keyCode === KEY_O && ConfigManager.mapScreenshotsEnabled && !capturing && SceneManager._scene instanceof Scene_Map ) { capturing = true; try { await captureFullMap(); } catch (err) { console.error(err); } capturing = false; } }); // --- Capture logic --- async function captureFullMap() { const scene = SceneManager._scene; const spriteset = scene._spriteset; const mapW = $gameMap.width() * $gameMap.tileWidth(); const mapH = $gameMap.height() * $gameMap.tileHeight(); const screenW = Graphics.width; const screenH = Graphics.height; const cropTop = Math.floor(screenH * CROP_TOP_FRAC); const cropRight = Math.floor(screenW * CROP_RIGHT_FRAC); const effW = screenW - cropRight; const effH = screenH - cropTop; const cols = Math.ceil(mapW / effW); const rows = Math.ceil(mapH / effH); const canvas = document.createElement("canvas"); canvas.width = mapW; canvas.height = mapH; const ctx = canvas.getContext("2d"); const oldX = $gameMap.displayX(); const oldY = $gameMap.displayY(); const oldPlayerX = $gamePlayer.x; const oldPlayerY = $gamePlayer.y; const exitPoints = collectExitPoints(); const originalUpdate = Scene_Map.prototype.update; Scene_Map.prototype.update = function () {}; const renderer = Graphics.app.renderer; const segments = []; for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { const dx = (x * effW) / $gameMap.tileWidth(); const dy = (y * effH) / $gameMap.tileHeight(); $gameMap.setDisplayPos(dx, dy); const tmpCanvas = await captureSegment(spriteset, scene, effW, effH, cropTop, FRAME_WAIT); ctx.drawImage(tmpCanvas, x * effW, y * effH); segments.push({ x, y, dx, dy, tmpCanvas }); } } await verifySegments(canvas, segments, effW, effH, spriteset, scene, cropTop); if (ConfigManager.mapScreenshotsShowLabels) { drawExitLabels(ctx, exitPoints); } Scene_Map.prototype.update = originalUpdate; $gameMap.setDisplayPos(oldX, oldY); $gamePlayer.center(oldPlayerX, oldPlayerY); saveCanvas(canvas); } async function captureSegment(spriteset, scene, effW, effH, cropTop, frameWait) { const renderer = Graphics.app.renderer; for (let f = 0; f < frameWait; f++) { scene.update(); spriteset.update(); Graphics.app.render(); await nextFrame(); } const snapCanvas = renderer.extract.canvas(spriteset); const tmp = document.createElement("canvas"); tmp.width = effW; tmp.height = effH; tmp.getContext("2d").drawImage(snapCanvas, 0, cropTop, effW, effH, 0, 0, effW, effH); return tmp; } async function verifySegments(canvas, segments, effW, effH, spriteset, scene, cropTop) { const ctx = canvas.getContext("2d"); for (const seg of segments) { const data = ctx.getImageData(seg.x * effW, seg.y * effH, effW, effH).data; let fullyDrawn = true; for (let i = 0; i < data.length; i += 4) { if (data[i + 3] === 0) { fullyDrawn = false; break; } } if (!fullyDrawn) { $gameMap.setDisplayPos(seg.dx, seg.dy); const fixed = await captureSegment(spriteset, scene, effW, effH, cropTop, FRAME_WAIT); ctx.drawImage(fixed, seg.x * effW, seg.y * effH); } } } function collectExitPoints() { const exits = []; const currentMapId = $gameMap.mapId(); for (const ev of $gameMap.events()) { if (!ev) continue; for (const page of ev.event().pages) { for (const cmd of page.list) { if (cmd.code === 201) { const destMapId = cmd.parameters[1]; if (destMapId === currentMapId) continue; const mapInfo = $dataMapInfos[destMapId]; if (mapInfo) exits.push({ name: mapInfo.name, x: ev.x, y: ev.y }); break; } } } } return exits; } function drawExitLabels(ctx, exits) { const tw = $gameMap.tileWidth(); const th = $gameMap.tileHeight(); ctx.font = "22px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "bottom"; const visited = new Set(); const clusters = []; for (let i = 0; i < exits.length; i++) { if (visited.has(i)) continue; const queue = [i]; const cluster = []; while (queue.length) { const idx = queue.shift(); if (visited.has(idx)) continue; visited.add(idx); cluster.push(exits[idx]); for (let j = 0; j < exits.length; j++) { if (visited.has(j)) continue; if (exits[j].name !== exits[idx].name) continue; const dx = Math.abs(exits[idx].x - exits[j].x); const dy = Math.abs(exits[idx].y - exits[j].y); if (Math.max(dx, dy) <= 1) queue.push(j); } } clusters.push(cluster); } for (const cluster of clusters) { cluster.sort((a, b) => (a.y - b.y) || (a.x - b.x)); const mid = cluster[Math.floor(cluster.length / 2)]; ctx.lineWidth = 2; ctx.strokeStyle = "red"; for (const ex of cluster) { const x = (ex.x + BOX_X_OFFSET) * tw; const y = (ex.y + BOX_Y_OFFSET) * th; ctx.strokeRect(x, y, tw, th); } const lines = wrapTextWords(mid.name, MAX_CHARS_PER_LINE); const blockHeight = lines.length * LINE_HEIGHT; let px = (mid.x + BOX_X_OFFSET + 0.5) * tw; let baseY = (mid.y + BOX_Y_OFFSET) * th - 4; const halfWidth = Math.max(...lines.map(l => ctx.measureText(l).width)) / 2; px = Math.max(halfWidth, Math.min(ctx.canvas.width - halfWidth, px)); if (baseY < blockHeight) baseY = blockHeight; for (let i = 0; i < lines.length; i++) { const y = baseY - (blockHeight - i * LINE_HEIGHT); ctx.lineWidth = 6; ctx.strokeStyle = "black"; ctx.strokeText(lines[i], px, y); ctx.lineWidth = 4; ctx.strokeText(lines[i], px, y); ctx.fillStyle = "#b862ffff"; ctx.fillText(lines[i], px, y); } } } function wrapTextWords(text, maxChars) { const words = text.split(" "); const lines = []; let current = ""; for (const word of words) { if ((current + (current ? " " : "") + word).length <= maxChars) { current += (current ? " " : "") + word; } else { if (current) lines.push(current); current = word; } } if (current) lines.push(current); return lines; } function nextFrame() { return new Promise(r => requestAnimationFrame(r)); } function saveCanvas(canvas) { if (!Utils.isNwjs()) return; const fs = require("fs"); const path = require("path"); const dir = path.join(process.cwd(), "(FullMapScreenshot)"); if (!fs.existsSync(dir)) fs.mkdirSync(dir); const file = path.join(dir, `map_${$gameMap.mapId()}_${Date.now()}.jpg`); const data = canvas.toDataURL("image/jpeg", 0.9).replace(/^data:image\/jpeg;base64,/, ""); fs.writeFileSync(file, Buffer.from(data, "base64")); } })();