Map Screenshotting mod

This commit is contained in:
onms 2025-12-22 03:23:15 -06:00
parent 8ec2a6d5a5
commit c8964aa859
3 changed files with 296 additions and 0 deletions

View file

@ -0,0 +1,6 @@
This is the folder that any images you take using the FullMapScreenshot mod will go.
The hotkey to activate this mod is "O". After hitting "O", this mod will render multiple pictures of the map you are currently on, stitch them together, save them to this folder as a ".jpg", and then relocate the camera back onto the character you are controlling.
By default, this mod will be disabled. If you want to use it, you will first need to toggle it on in the game's options (this will be memorized for future game launches).
There is also an additional feature which displays connected locations. If you want to take a clean screenshot of the map, you can toggle this feature off.

View file

@ -2551,4 +2551,10 @@ var $plugins = [
"Toggle Key": "77",
},
},
{
"name": "FullMapScreenshot",
"status": true,
description: "Press O to take a screenshot of the current map, which will be saved to (FMS Mod Screenshots)",
"parameters": {},
},
];

View file

@ -0,0 +1,284 @@
/*:
* @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"));
}
})();